[b1a4 팀프로젝트 TIL] 251001수(6) 배느실

2025. 10. 1. 21:35·2025/[풀스택]SeSAC 웹개발자 7기
반응형

251001수(6)

프로젝트 팀 명 : b1a4

수업 일자 : 2025. 10. 01 수

성명 : 김도이

나의 프로젝트 수업 점수 : / 100

오늘 할 일

  • [ ] User - role 기반 Authorization(인가) 설정
  • [ ] User - 회원가입 성공 후, 이메일 인증
  • [ ] User - 로그아웃 기능
  • [ ] Cafe - CRUD
  • [x] 크롤링 공부
  • [x] 카페 데이터 python 크롤링 시작
  • [ ] User - 탈퇴 기능(soft delete?)
  • [x] 카페 데이터들 DB에 저장
  • [x] 카페 데이터 DB 확인
  • DB 저장 시 경도/위도 값 String → DECIMAL 변환 처리예시:
    왜 String일까?
    • JSON 응답에서 소수점 정밀도를 그대로 보존하기 위해 문자열로 전달하는 겁니다.
    • 만약 float으로 내려주면 자릿수가 잘리거나 언어별 파싱에서 오차가 생길 수 있거든요.

    DB 저장할 때는?따라서 DB에서는 DECIMAL 타입으로 변환해서 저장하는 게 맞습니다.
    latitude  DECIMAL(20,15) NOT NULL,   -- 위도
    longitude DECIMAL(20,15) NOT NULL    -- 경도
    
    
    • DECIMAL(20,15) → 소수점 이하 15자리까지 정밀하게 저장 가능
    • 위도/경도는 최대 180, 90 이내 값이라 정수 자리는 3자리면 충분, 나머지는 소수 자릿수로 커버
    👉 사실 현실적으로는 DECIMAL(10,7) 정도만 해도 GPS 좌표는 ±1cm 수준까지 표현 가능합니다.
    파이썬에서 변환
    lat = float(doc.get("y"))   # 위도
    lng = float(doc.get("x"))   # 경도
    
    

    📌 정리
    • 카카오맵 API는 좌표를 String으로 주지만
    • DB에서는 DECIMAL(10,7) ~ DECIMAL(20,15) 형태로 바꿔 저장하는 게 맞음
    • 그래야 나중에 ST_Distance_Sphere 같은 공간쿼리, 반경 검색에서 성능도 좋아짐
  • API로 받아온 String → float 변환 후 DB에 저장하면 됩니다:
  • 추천 스키마
  • 실제 서비스에서는 좌표는 숫자 연산(거리 계산, 근접 검색) 에 많이 쓰입니다.
  • { "x": "127.05902969025047", "y": "37.51207412593136" }
  • 카카오맵 REST API 응답 보면 x, y, latitude, longitude 전부 String 타입으로 내려와요.
  • [ ] 핵심 구 별로 데이터들 DB에 저장
  1. 강남구 → 김도이
  2. 마포구
  3. 서초구
  4. 성동구
  5. 광진구
  6. 송파구
  7. 용산구
  8. 관악구
  9. 금천구
  10. 영등포구 → 박민재
  11. 구로구
  12. 강서구
  13. 양천구
  14. 동작구
  15. 동대문구
  16. 성북구
  17. 서대문구
  18. 종로구 → 김가연
  19. 중구
  20. 강동구
  21. 중랑구
  22. 도봉구
  23. 은평구
  24. 노원구
  25. 강북구
  • [ ] 카페API CRUD 시작 ⁃ 유저는 카페정보 조회/등록요청 가능 ⁃ 관리자는 카페정보 등록/수정/삭제 가능 ⁃ 없어진카페정보 신고기능(QNA?)
  • [ ] 카페오픈시간은 어떻게?
  • [ ] 카페 이미지들 어떻게? ⁃ 메뉴판 ⁃ 분위기 및 외관/내부사진 ⁃ 음식 사진
  • [ ] 카페후기 요약은 어떻게?
  • [ ] 카페와 어울리는 태그 추출은 어떻게?

1) 프로젝트 수업에서 배운점

강남구 카페 크롤링하는데만 최소30분~2시간(?) 걸릴듯...

  • 파이썬 ㅋㅋㅋㅋㅋㅋ
    • 와… 세상에 파이썬을 배우지도 않았는데 이렇게 바로 사용하게 될 줄은 몰랐다. 전적으로 ‘크롤링’을 위해서만 사용하는데, 그래도 이렇게 경험해보아서 되게 기쁘다!
    • 실제로 파이썬으로 카카오맵 REST API 검색 사용해서 카페 정보들까지 DB에 저장시키는 작업을 해보니 너무 재밌고 뿌듯하다.

2) 프로젝트 수업에서 느낀점

  • 언어 몇 개를 제대로 다룰 수 있을 정도로 해두니, 문법만 조금 바뀔 뿐이지 새로운 언어에 적응하는 것이 생각보다 어렵고 겁이 나는 일이 아니었다는 것을 느꼈다!
    • 아마.. 언어 중 제일 쉬워서 입문자들에게 추천한다는 그 Python이어서 그런 게 아닌가 싶기도 하다^^…

3) 프로젝트 수업에서 실천할 점

  • 이번 주 내로 공유DB 디깅해서 파고, 각 구 별 코드 돌려놓기(인당 8개씩 나눠야 할 것 같음)

다음 할 일

  • [ ] DB
    • [ ] 2.논리적설계(ERD모델링) : 개념적 설계를 기반으로 ERD를 작성 / 엔티티, 속성, 관계 카디널리티(1:1, 1:N, N:M)를 명확히
    • [ ] 3.물리적설계(테이블) : ERD를 실제 DBMS에 맞게 변환 / 테이블명, 컬럼명, 데이터타입, PK/FK, 제약조건, 인덱스까지 반영
  • [ ] 로그아웃 완성 및 통합 테스트 추가
  • [ ] 토큰 재발급 API 구현(Refresh Token 회전+재사용 감지)
  • [ ] 입력 검증(@Valid DTO + 글로벌 예외 처리) 및 에러 응답 포맷 통일
  • [ ] 이메일 인증/비밀번호 재설정 플로우 도입
  • [ ] me 프로필 조회/수정, 중복 체크 API 제공
  • [ ] 보안 하드닝(로그인 시도 제한, 감사 로그, 토큰 블랙리스트 또는 짧은 Access 정책)
  • [ ] (민재)필요 시 소셜 로그인/(가연)관리자 기능 확장
  • [ ] AI활용 - 어디까지가 가능한 기능인지 확인 필요
  • [ ] 필요한 API들 둘러보기
  • 인텔리제이 AI 채팅구현 완료
    • 회원가입 POST /api/auth/signup
      • UUID로 userId 발급
      • 비밀번호 암호화 저장(PasswordEncoder)
      • 기본값 설정: status=ACTIVE, role=USER, provider=LOCAL
      • 이메일 중복 검증(Service에서 existsByEmail 체크)
      • 성공 시 201 반환 및 응답 DTO 구성
    • 로그인 POST /api/auth/login
      • 이메일/비밀번호 인증(Service에서 encoder.matches 사용)
      • JWT Access/Refresh 토큰 발급(TokenProvider.issueTokens)
      • DB에 refreshToken 저장(UserService.update)
      • 성공 시 200 반환 및 토큰 DTO 응답
    • 저장소/서비스 레이어
      • UserRepository: existsByEmail, findByEmail, findByRefreshToken 등 기본 쿼리
      • UserService: 회원 생성(create), 로그인 인증(getByCredentials), 갱신(update), refreshToken으로 조회(getByRefreshToken)
    부분 구현(미완성/버그)
    • 로그아웃
      • 컨트롤러 메서드가 존재하지만 다음이 누락됨:
        • @PostMapping("/logout") 등의 매핑 없슴
        • refreshToken 삭제 후 DB 업데이트(userService.update) 누락
        • 정상/에러 응답 반환 누락(현재 메서드가 return 경로 없이 종료)
        • 에러 메시지 오타: "refreshToekn"
        • 예외 처리 로깅/메시지 부족
    아직 미구현(일반적으로 필요한 항목)
    • 토큰 재발급(Refresh Token → 새로운 Access/Refresh 발급, 회전/재사용 감지)
    • 이메일 인증(회원가입 후 이메일 검증) 및 인증 상태 관리
    • 비밀번호 변경(현재 비밀번호 확인 후 변경)
    • 비밀번호 초기화(비밀번호 찾기/재설정: 이메일 발송 링크, 토큰)
    • 중복 체크 API(이메일/닉네임 사용 가능 여부)
    • 내 정보 조회/수정(me 프로필 조회, 닉네임/프로필 이미지 등)
    • 로그아웃 전체(모든 기기에서 로그아웃: 모든 refreshToken 무효화 전략)
    • 계정 비활성화/삭제(soft delete 또는 상태 전환)
    • 권한/역할 관리(ADMIN 권한으로 사용자 관리 등)
    • 소셜 로그인(OAuth: Google/Kakao/Naver) 및 provider_id 연동
    • 보안 하드닝
      • 입력값 유효성 검증(Bean Validation @Valid + ControllerAdvice로 표준 에러 응답)
      • 로그인 실패 횟수 제한/잠금, rate limiting, 브루트포스 방지
      • 토큰 블랙리스트/짧은 Access + 긴 Refresh + 회전 정책
      • CORS/보안 헤더 설정
    • 감사/감사 로그, 접속 기록, 마지막 로그인 시각 등 운영 관점 필드
    • 스웨거 문서: 응답 스키마/에러 포맷 표준화, 공통 예시/스키마 재사용
    우선 적용 권장 수정(Logout 완성)
    • 엔드포인트 추가, refreshToken 검증 → 해당 사용자 조회 → refreshToken 제거 → 저장 → 표준 응답 반환
    // ... existing code ...
        @PostMapping("/login")
        public ResponseEntity<?> login(
                @io.swagger.v3.oas.annotations.parameters.RequestBody(
                        required = true,
                        content = @io.swagger.v3.oas.annotations.media.Content(
                                mediaType = org.springframework.http.MediaType.APPLICATION_JSON_VALUE,
                                schema = @io.swagger.v3.oas.annotations.media.Schema(
                                        implementation = com.b1a4.cafeOn.dto.UserDTO.class
                                ),
                                examples = {
                                        @io.swagger.v3.oas.annotations.media.ExampleObject(
                                                name = "로그인 요청 예시",
                                                value = """
                                                        {
                                                            "email": "user@example.com",
                                                            "password": "P@ssw0rd!"
                                                        }
                                                        """
                                        )
                                }
                        )
                )
                @RequestBody UserDTO userDTO)
        {
            UserEntity user = userService.getByCredentials( // 사용자 인증하는 메서드
                    userDTO.getEmail(), userDTO.getPassword(),passwordEncoder
            );
    
            if (user != null) { // DB에서 해당 email, password가 일치하는 유저가 있으면,
                final Map<String, String> token = tokenProvider.issueTokens(user);
                user.setRefreshToken(token.get("refreshToken"));
                userService.update(user);
    
                final UserDTO responseUserDTO = UserDTO.builder()
                        .token(token.get("accessToken"))
                        .refreshToken(token.get("refreshToken"))
                        .build();
    
                ApiResponse<UserDTO> response = ApiResponse.<UserDTO>builder()
                        .message("로그인 성공")
                        .data(responseUserDTO)
                        .build();
    
                return ResponseEntity.ok().body(response);
            } else {
                ApiResponse<Void> response = ApiResponse.<Void>builder()
                        .message("로그인 실패")
                        .build();
    
                return ResponseEntity.badRequest().body(response);
            }
        }
    // ... existing code ...
        @Operation(
                summary = "로그아웃",
                description = "Refresh Token을 무효화하여 로그아웃합니다.",
                requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody(
                        required = true,
                        content = @io.swagger.v3.oas.annotations.media.Content(
                                mediaType = org.springframework.http.MediaType.APPLICATION_JSON_VALUE,
                                schema = @io.swagger.v3.oas.annotations.media.Schema(
                                        implementation = java.util.Map.class
                                ),
                                examples = {
                                        @io.swagger.v3.oas.annotations.media.ExampleObject(
                                                name = "로그아웃 요청 예시",
                                                value = """
                                                {
                                                  "refreshToken": "여기에-리프레시-토큰"
                                                }
                                                """
                                        )
                                }
                        )
                )
        )
        @PostMapping("/logout")
        public ResponseEntity<?> logout(@RequestBody Map<String, String> request) {
            try {
                String refreshToken = request.get("refreshToken");
    
                if (refreshToken == null || refreshToken.isEmpty()) {
                    return ResponseEntity.badRequest().body(
                            ApiResponse.<Void>builder()
                                    .message("로그아웃 실패: refreshToken이 필요합니다.")
                                    .build()
                    );
                }
    
                UserEntity user = userService.getByRefreshToken(refreshToken);
                if (user == null) {
                    return ResponseEntity.badRequest().body(
                            ApiResponse.<Void>builder()
                                    .message("로그아웃 실패: 해당 refreshToken을 가진 유저가 없습니다.")
                                    .build()
                    );
                }
    
                // refreshToken 제거(=로그아웃)
                user.setRefreshToken(null);
                userService.update(user);
    
                return ResponseEntity.ok(
                        ApiResponse.<Void>builder()
                                .message("로그아웃 성공")
                                .build()
                );
            } catch (Exception e) {
                log.error("로그아웃 처리 중 오류", e);
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
                        ApiResponse.<Void>builder()
                                .message("로그아웃 실패: 서버 오류")
                                .build()
                );
            }
        }
    // ... existing code ...
    
    
    다음 단계 제안(우선순위)
    1. 로그아웃 완성 및 통합 테스트 추가
    2. 토큰 재발급 API 구현(Refresh Token 회전+재사용 감지)
    3. 입력 검증(@Valid DTO + 글로벌 예외 처리) 및 에러 응답 포맷 통일
    4. 이메일 인증/비밀번호 재설정 플로우 도입
    5. me 프로필 조회/수정, 중복 체크 API 제공
    6. 보안 하드닝(로그인 시도 제한, 감사 로그, 토큰 블랙리스트 또는 짧은 Access 정책)
    7. 필요 시 소셜 로그인/관리자 기능 확장
    원하시면 각 항목의 설계 스케치나 추가 코드 템플릿도 바로 드리겠습니다.
  • 다음은 현재 User 관련 API의 구현 현황과 미완/필요 항목을 정리한 것입니다.
저작자표시 비영리 변경금지 (새창열림)

'2025 > [풀스택]SeSAC 웹개발자 7기' 카테고리의 다른 글

✅ (메일링서비스) Gmail SMTP 발송 한도?  (0) 2025.10.09
✅ DTO란?  (0) 2025.10.09
서울시 25개 전체 구 카페정보를 최대한 많이 수집하는 전략?  (0) 2025.10.01
API 기반 크롤링?  (0) 2025.10.01
python 크롤링?  (1) 2025.10.01
'2025/[풀스택]SeSAC 웹개발자 7기' 카테고리의 다른 글
  • ✅ (메일링서비스) Gmail SMTP 발송 한도?
  • ✅ DTO란?
  • 서울시 25개 전체 구 카페정보를 최대한 많이 수집하는 전략?
  • API 기반 크롤링?
d0yclub
d0yclub
2024.06.17 open
  • d0yclub
    개발꿈나무 김도이
    d0yclub
  • 전체
    오늘
    어제
    • 분류 전체보기 (77)
      • 서재 (0)
      • 2024 (13)
        • [FE]Next.js2기 (6)
        • [FE]우아한테크코스7기-프리코스 (6)
      • 개발공부 (25)
        • HTML CSS (6)
        • JavaScript (2)
        • React.js (4)
        • DB - mySQL (0)
        • error.log (8)
      • 2025 (32)
        • 멋쟁이사자처럼 프론트엔드스쿨플러스 4기 (1)
        • [풀스택]SeSAC 웹개발자 7기 (29)
        • 개인프로젝트 (1)
        • 팀프로젝트 (1)
      • 자료구조&알고리즘 풀이 (3)
        • 프로그래머스 (2)
      • 2026 (0)
        • [AI]SeSAC microsoft AI엔지니어 .. (0)
        • 개인프로젝트 (0)
        • 팀프로젝트 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    프로젝트캠프
    부트캠프
    GCP
    Next.js
    프론트엔드개발자양성과정
    TIL
    KPT
    웅진씽크빅
    스나이퍼팩토리
    KPT회고
    미래내일일경험
    EC2
    배느실
    udemy
    회고
    팀프로젝트
    유데미
    RDS
    DTO
    트러블슈팅
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
d0yclub
[b1a4 팀프로젝트 TIL] 251001수(6) 배느실
상단으로

티스토리툴바