반응형
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자리면 충분, 나머지는 소수 자릿수로 커버
파이썬에서 변환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에 저장
강남구→ 김도이- 마포구
- 서초구
- 성동구
- 광진구
- 송파구
- 용산구
- 관악구
- 금천구
- 영등포구 → 박민재
- 구로구
- 강서구
- 양천구
- 동작구
- 동대문구
- 성북구
- 서대문구
- 종로구 → 김가연
- 중구
- 강동구
- 중랑구
- 도봉구
- 은평구
- 노원구
- 강북구
- [ ] 카페API CRUD 시작 ⁃ 유저는 카페정보 조회/등록요청 가능 ⁃ 관리자는 카페정보 등록/수정/삭제 가능 ⁃ 없어진카페정보 신고기능(QNA?)
- [ ] 카페오픈시간은 어떻게?
- [ ] 카페 이미지들 어떻게? ⁃ 메뉴판 ⁃ 분위기 및 외관/내부사진 ⁃ 음식 사진
- [ ] 카페후기 요약은 어떻게?
- [ ] 카페와 어울리는 태그 추출은 어떻게?
1) 프로젝트 수업에서 배운점

- 파이썬 ㅋㅋㅋㅋㅋㅋ
- 와… 세상에 파이썬을 배우지도 않았는데 이렇게 바로 사용하게 될 줄은 몰랐다. 전적으로 ‘크롤링’을 위해서만 사용하는데, 그래도 이렇게 경험해보아서 되게 기쁘다!
- 실제로 파이썬으로 카카오맵 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/보안 헤더 설정
- 감사/감사 로그, 접속 기록, 마지막 로그인 시각 등 운영 관점 필드
- 스웨거 문서: 응답 스키마/에러 포맷 표준화, 공통 예시/스키마 재사용
- 엔드포인트 추가, 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 ...- 로그아웃 완성 및 통합 테스트 추가
- 토큰 재발급 API 구현(Refresh Token 회전+재사용 감지)
- 입력 검증(@Valid DTO + 글로벌 예외 처리) 및 에러 응답 포맷 통일
- 이메일 인증/비밀번호 재설정 플로우 도입
- me 프로필 조회/수정, 중복 체크 API 제공
- 보안 하드닝(로그인 시도 제한, 감사 로그, 토큰 블랙리스트 또는 짧은 Access 정책)
- 필요 시 소셜 로그인/관리자 기능 확장
- 회원가입 POST /api/auth/signup
- 다음은 현재 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 |