diff --git a/src/main/java/com/joycrew/backend/controller/CatalogController.java b/src/main/java/com/joycrew/backend/controller/CatalogController.java index 4d37ba2..4e889a3 100644 --- a/src/main/java/com/joycrew/backend/controller/CatalogController.java +++ b/src/main/java/com/joycrew/backend/controller/CatalogController.java @@ -23,10 +23,13 @@ public ResponseEntity> listKakaoByCategory( @PathVariable String category, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, - @RequestParam(defaultValue = "POPULAR") SortOption sort + @RequestParam(defaultValue = "POPULAR") SortOption sort, + @RequestParam(required = false) String searchName // 상품명 검색 파라미터 추가 ) { GiftCategory gc = GiftCategory.valueOf(category.toUpperCase()); - return ResponseEntity.ok(catalog.listByCategory(gc, page, size, sort)); + + // 서비스 레이어에도 searchName 파라미터 전달 (ExternalCatalogService.listByCategory 수정 필요) + return ResponseEntity.ok(catalog.listByCategory(gc, page, size, sort, searchName)); } @GetMapping(value = "/kakao/product/{templateId}", produces = "application/json; charset=UTF-8") @@ -34,4 +37,4 @@ public ResponseEntity detailPoints(@PathVariable var resp = catalog.getDetailWithPoints(templateId); return resp == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(resp); } -} +} \ No newline at end of file diff --git a/src/main/java/com/joycrew/backend/controller/KycController.java b/src/main/java/com/joycrew/backend/controller/KycController.java index e28abbb..a7442e0 100644 --- a/src/main/java/com/joycrew/backend/controller/KycController.java +++ b/src/main/java/com/joycrew/backend/controller/KycController.java @@ -1,9 +1,6 @@ package com.joycrew.backend.controller; -import com.joycrew.backend.dto.kyc.PhoneStartRequest; -import com.joycrew.backend.dto.kyc.PhoneStartResponse; -import com.joycrew.backend.dto.kyc.PhoneVerifyRequest; -import com.joycrew.backend.dto.kyc.PhoneVerifyResponse; +import com.joycrew.backend.dto.kyc.*; import com.joycrew.backend.service.PhoneVerificationService; import com.joycrew.backend.repository.EmployeeRepository; import com.joycrew.backend.util.EmailMasker; @@ -11,7 +8,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; -import java.time.LocalDateTime; import java.util.Comparator; import java.util.List; import java.util.Objects; @@ -35,25 +31,30 @@ public PhoneVerifyResponse verify(@RequestBody @Valid PhoneVerifyRequest req) { // 1) 코드 검증 + KYC 토큰 생성 + phone 획득 var r = svc.verify(req.requestId(), req.code()); - // 2) 해당 phone으로 직원들 조회 → 이메일/최근로그인 추출 + // 2) 해당 phone으로 직원들 조회 var employees = employeeRepo.findByPhoneNumber(r.phone()); - List emails = employees.stream() - .flatMap(e -> Stream.of(e.getEmail(), e.getPersonalEmail())) - .filter(Objects::nonNull) - .map(EmailMasker::mask) - .distinct() + // 3) [수정된 로직] + // Employee 엔티티를 VerifiedEmailInfo DTO 리스트로 변환 + // (회사 이메일과 개인 이메일을 별도 항목으로 취급) + List accounts = employees.stream() + .flatMap(e -> Stream.of( + // 회사 이메일 정보 + new VerifiedEmailInfo(EmailMasker.mask(e.getEmail()), e.getLastLoginAt()), + // 개인 이메일 정보 (null이 아닐 경우에만 생성) + (e.getPersonalEmail() != null) + ? new VerifiedEmailInfo(EmailMasker.mask(e.getPersonalEmail()), e.getLastLoginAt()) + : null + )) + .filter(Objects::nonNull) // personalEmail이 null이었던 스트림 제거 + .filter(info -> info.email() != null) // 마스킹된 이메일이 null이 아닌 경우 + .distinct() // (이메일, 날짜)가 완전히 동일한 경우 중복 제거 + // 최근 로그인 날짜 기준으로 내림차순 정렬 (프론트 편의성) + .sorted(Comparator.comparing(VerifiedEmailInfo::recentLoginAt, Comparator.nullsLast(Comparator.reverseOrder()))) .toList(); - LocalDateTime recent = employees.stream() - .map(e -> e.getLastLoginAt()) // LocalDateTime - .filter(Objects::nonNull) - .max(Comparator.naturalOrder()) - .orElse(null); - - String recentStr = (recent != null) ? recent.toString() : null; // ISO-8601 문자열 - - // 3) 응답 - return new PhoneVerifyResponse(r.verified(), r.kycToken(), emails, recentStr); + // 4) [수정된 생성자 호출] (3-인수) + return new PhoneVerifyResponse(r.verified(), r.kycToken(), accounts); } } + diff --git a/src/main/java/com/joycrew/backend/dto/kyc/PhoneVerifyResponse.java b/src/main/java/com/joycrew/backend/dto/kyc/PhoneVerifyResponse.java index bdd54ea..66510af 100644 --- a/src/main/java/com/joycrew/backend/dto/kyc/PhoneVerifyResponse.java +++ b/src/main/java/com/joycrew/backend/dto/kyc/PhoneVerifyResponse.java @@ -2,9 +2,11 @@ import java.util.List; +/** + * 핸드폰 인증 완료 시 최종 응답 DTO + */ public record PhoneVerifyResponse( boolean verified, String kycToken, - List emails, - String recentLoginAt + List accounts // 기존 List emails, String recentLoginAt -> List accounts ) {} \ No newline at end of file diff --git a/src/main/java/com/joycrew/backend/dto/kyc/VerifiedEmailInfo.java b/src/main/java/com/joycrew/backend/dto/kyc/VerifiedEmailInfo.java new file mode 100644 index 0000000..e05f3c1 --- /dev/null +++ b/src/main/java/com/joycrew/backend/dto/kyc/VerifiedEmailInfo.java @@ -0,0 +1,11 @@ +package com.joycrew.backend.dto.kyc; + +import java.time.LocalDateTime; + +/** + * KYC(본인인증) 완료 시 반환하는 계정(이메일)과 최근 로그인 날짜 DTO + */ +public record VerifiedEmailInfo( + String email, + LocalDateTime recentLoginAt // JSON 직렬화 시 ISO-8601 날짜 문자열로 자동 변환 +) {} diff --git a/src/main/java/com/joycrew/backend/exception/GlobalExceptionHandler.java b/src/main/java/com/joycrew/backend/exception/GlobalExceptionHandler.java index 962665c..d8c7465 100644 --- a/src/main/java/com/joycrew/backend/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/joycrew/backend/exception/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.userdetails.UsernameNotFoundException; // import 추가 import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; @@ -13,32 +14,45 @@ @RestControllerAdvice public class GlobalExceptionHandler { - @ExceptionHandler(InsufficientPointsException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorResponse handleInsufficientPoints(InsufficientPointsException ex, HttpServletRequest req) { - return new ErrorResponse("INSUFFICIENT_POINTS", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); - } - - @ExceptionHandler(NoSuchElementException.class) - @ResponseStatus(HttpStatus.NOT_FOUND) - public ErrorResponse handleNoSuchElement(NoSuchElementException ex, HttpServletRequest req) { - return new ErrorResponse("NOT_FOUND", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); - } - - @ExceptionHandler(IllegalStateException.class) - @ResponseStatus(HttpStatus.BAD_REQUEST) - public ErrorResponse handleIllegalState(IllegalStateException ex, HttpServletRequest req) { - return new ErrorResponse("ORDER_CANNOT_CANCEL", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); - } - - @ExceptionHandler(BadCredentialsException.class) - public ResponseEntity handleAuthenticationFailed(BadCredentialsException ex, HttpServletRequest req) { // 1. HttpServletRequest 추가 - ErrorResponse errorResponse = new ErrorResponse( - "AUTHENTICATION_FAILED", - ex.getMessage(), - LocalDateTime.now(), - req.getRequestURI() - ); - return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); - } -} + @ExceptionHandler(InsufficientPointsException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleInsufficientPoints(InsufficientPointsException ex, HttpServletRequest req) { + return new ErrorResponse("INSUFFICIENT_POINTS", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); + } + + @ExceptionHandler(NoSuchElementException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ErrorResponse handleNoSuchElement(NoSuchElementException ex, HttpServletRequest req) { + return new ErrorResponse("NOT_FOUND", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); + } + + @ExceptionHandler(IllegalStateException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleIllegalState(IllegalStateException ex, HttpServletRequest req) { + return new ErrorResponse("ORDER_CANNOT_CANCEL", ex.getMessage(), LocalDateTime.now(), req.getRequestURI()); + } + + // '가입되지 않은 이메일' 처리 + @ExceptionHandler(UsernameNotFoundException.class) + public ResponseEntity handleUsernameNotFound(UsernameNotFoundException ex, HttpServletRequest req) { + ErrorResponse errorResponse = new ErrorResponse( + "AUTH_002", // 가입되지 않은 이메일 + "이메일 또는 비밀번호가 잘못되었습니다.", // 보안을 위해 메시지는 동일하게 유지 + LocalDateTime.now(), + req.getRequestURI() + ); + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } + + // '비밀번호 불일치' 처리 + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleAuthenticationFailed(BadCredentialsException ex, HttpServletRequest req) { + ErrorResponse errorResponse = new ErrorResponse( + "AUTH_003", // 비밀번호 불일치 + "이메일 또는 비밀번호가 잘못되었습니다.", // 보안을 위해 메시지는 동일하게 유지 + LocalDateTime.now(), + req.getRequestURI() + ); + return new ResponseEntity<>(errorResponse, HttpStatus.UNAUTHORIZED); + } +} \ No newline at end of file diff --git a/src/main/java/com/joycrew/backend/repository/KakaoTemplateRepository.java b/src/main/java/com/joycrew/backend/repository/KakaoTemplateRepository.java index 9540cac..f0bdd69 100644 --- a/src/main/java/com/joycrew/backend/repository/KakaoTemplateRepository.java +++ b/src/main/java/com/joycrew/backend/repository/KakaoTemplateRepository.java @@ -6,5 +6,10 @@ import org.springframework.data.jpa.repository.JpaRepository; public interface KakaoTemplateRepository extends JpaRepository { + + // Existing method Page findByJoyCategory(GiftCategory category, Pageable pageable); + + // [NEW] Added method for searching by name (resolves error in ExternalCatalogService) + Page findByJoyCategoryAndNameContainingIgnoreCase(GiftCategory category, String name, Pageable pageable); } diff --git a/src/main/java/com/joycrew/backend/service/ExternalCatalogService.java b/src/main/java/com/joycrew/backend/service/ExternalCatalogService.java index e90c998..81baccd 100644 --- a/src/main/java/com/joycrew/backend/service/ExternalCatalogService.java +++ b/src/main/java/com/joycrew/backend/service/ExternalCatalogService.java @@ -10,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.*; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; // 1. StringUtils import 추가 import java.util.List; @@ -22,14 +23,28 @@ public class ExternalCatalogService { @Value("${joycrew.points.krw_per_point:40}") private int krwPerPoint; - public List listByCategory(GiftCategory category, int page, int size, SortOption sort) { + // 2. 메소드 시그니처에 searchName 파라미터 추가 + public List listByCategory(GiftCategory category, int page, int size, SortOption sort, String searchName) { Sort s = switch (sort) { case PRICE_ASC -> Sort.by(Sort.Direction.ASC, "basePriceKrw"); case PRICE_DESC -> Sort.by(Sort.Direction.DESC, "basePriceKrw"); case POPULAR, NEW -> Sort.by(Sort.Direction.DESC, "updatedAt"); }; - Page p = templateRepo.findByJoyCategory(category, PageRequest.of(page, size, s)); + Pageable pageRequest = PageRequest.of(page, size, s); + Page p; + + // 3. searchName (검색어) 유무에 따라 분기 처리 + if (StringUtils.hasText(searchName)) { + // 검색어가 있는 경우: 이름으로 검색 + // (참고: KakaoTemplateRepository에 이 메소드가 정의되어 있어야 합니다) + p = templateRepo.findByJoyCategoryAndNameContainingIgnoreCase(category, searchName, pageRequest); + } else { + // 검색어가 없는 경우: 기존 로직 (카테고리로만 조회) + p = templateRepo.findByJoyCategory(category, pageRequest); + } + + // 4. 조회 결과를 DTO로 변환하여 반환 return p.getContent().stream().map(t -> { int point = (int) Math.ceil(t.getBasePriceKrw() / (double) krwPerPoint); return new ExternalProductResponse( @@ -53,3 +68,4 @@ public ExternalProductDetailResponse getDetailWithPoints(String templateId) { ); } } + diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index e69de29..6a3feec 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,74 @@ +# H2 Database settings for 'dev' profile +spring: + # --- H2 Datasource Configuration --- + datasource: + # Use H2 in-memory database named 'testdb' + # DB_CLOSE_DELAY=-1 keeps the database alive as long as the JVM is running + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 + # H2 JDBC Driver + driver-class-name: org.h2.Driver + # Default H2 username + username: sa + # Default H2 password (can be empty or set) + password: password + + # --- H2 Console Configuration (Optional but recommended) --- + h2: + console: + # Enable H2 web console + enabled: true + # Access path for H2 console (e.g., http://localhost:8082/h2-console) + path: /h2-console + settings: + # Allows access from web browsers + web-allow-others: false + # Disables trace output in console + trace: false + + # --- JPA/Hibernate Configuration --- + jpa: + # Let Hibernate automatically detect H2 dialect (no need to specify) + # database-platform: org.hibernate.dialect.H2Dialect # Optional + hibernate: + # Automatically update schema based on entities (create/update tables) + ddl-auto: update + properties: + hibernate: + # format_sql: true # Optional: Pretty print SQL logs + # use_sql_comments: true # Optional: Add comments to SQL logs + # Show executed SQL statements in the logs (optional) + show-sql: true + +# --- Other configurations (like jwt, mail, etc. remain the same) --- +jwt: + expiration-ms: 3600000 + password-reset-expiration-ms: 900000 + secret: ${JWT_SECRET:defaultSecretValueForDev} # Provide a default for dev if env var is not set + +app: + frontend-url: http://localhost:3000 # Use localhost for dev frontend + sms: + provider: solapi + from-number: 123 + kyc: + token-secret: 123 + token-ttl-minutes: 10 + +solapi: + api-key: 123 + api-secret: 123 + base-url: "https://api.solapi.com" + +kakao: + giftbiz: + base-url: 123 + api-key: 123 + timeout-ms: 5000 + callback: + success-url: 123 + fail-url: 123 + gift-cancel-url: 123 + +joycrew: + points: + krw_per_point: ${JOYCREW_POINTS_KRW_PER_POINT:40} \ No newline at end of file