Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1177271
cicd: docker-compose.yml
juanxiu Oct 5, 2025
473dde2
cicd: healthcheck
juanxiu Oct 5, 2025
64b3bb4
feat: 매칭 카드 조회 시 userId 하드코딩 제거
juanxiu Oct 10, 2025
55d4870
feat: userId를 AuthUtils에서 가져오도록
juanxiu Oct 10, 2025
c9d5839
feat: 푸시알림으로부터 소비 내역 저장 TODO 추가
juanxiu Oct 10, 2025
0884c30
cicd: EC2 docker 설정 파일로 로컬 파일 변경
juanxiu Oct 11, 2025
f15001a
feat: proto 파일 변경
juanxiu Oct 11, 2025
6a7de02
feat: proto 데이터에 맞게 entity 필드 수정
juanxiu Oct 11, 2025
834da0e
feat: 변경된 cardData, cardCompany.proto 에 맞게 이벤트 컴포넌트 및 관련 엔티티, dto 변경
juanxiu Oct 12, 2025
0c63207
feat: 비즈니스 로직 수정
juanxiu Oct 13, 2025
15de794
feat:PromotionServiceImpl
juanxiu Oct 14, 2025
8388b23
feat: StoreInfoDTO 매장 위치 정보 dto 필드 추가
juanxiu Oct 15, 2025
a5211f1
fix: jpa 테이블 설정
juanxiu Oct 15, 2025
dd82c55
feat: PromotionEventListener 추가
juanxiu Oct 15, 2025
cfc8b9f
feat: application.yml mysql datasource 로
juanxiu Oct 15, 2025
8e8e179
feat: deploy.yml에 디비네임 수정
juanxiu Oct 15, 2025
41cf4bb
fix: 각 세부 혜택 데이터베이스에 저장 및 레포지토리 타입 참조 해결
juanxiu Oct 15, 2025
9d3a03f
fix: 카드데이터 비동기 처리 제거
juanxiu Oct 15, 2025
e0b06ed
fix: 프로모션 리스너 어노테이션
juanxiu Oct 15, 2025
53c941d
fix: 프로모션 데이터 예외처리 개선
juanxiu Oct 15, 2025
91c8e6d
fix: Benefit 엔티티 생성 시 Card 도 함꼐 전달
juanxiu Oct 15, 2025
dab0be3
fix: 데이터 변경 감지 로직 없이 곧바로 수신 후 저장되도록
juanxiu Oct 15, 2025
e5fe0f9
feat: summary 필드 타입 TEXT 로 변경
juanxiu Oct 15, 2025
697b912
feat: 카드 등록 시 UserCard 엔티티 생성할 때 registeredAt 추가
juanxiu Oct 15, 2025
1325389
logs: 디버깅용 로그 추가
juanxiu Oct 15, 2025
9b1d5ee
feat: 카카오 API 호출 시 query 파라미터가 필수 필드, "매장"으로 하드코딩
juanxiu Oct 15, 2025
7a807d2
feat: 카카오 검색 로그
juanxiu Oct 15, 2025
5f4861f
feat: 카카오 검색 카테고리 제거, 반경 5KM 로
juanxiu Oct 15, 2025
d5ca147
feat: 카카오 호출 쿼리 ""로 변경
juanxiu Oct 15, 2025
2fb875a
feat: 카카오 API URL 변경
juanxiu Oct 15, 2025
97821a9
feat: StoreInfo 객체 생성할 때 위치 정보 포함
juanxiu Nov 4, 2025
5b07aeb
feat: 회원가입&로그인 login에 통합
sominyun Nov 4, 2025
5febf7e
feat: 로직 수정
juanxiu Nov 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,13 @@ jobs:
# 환경변수 파일 업데이트
cat > .env << EOF
DB_HOST=${{ secrets.DB_HOST }}
DB_NAME=db-wisecard
DB_NAME=wisecarddb
DB_USERNAME=admin
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
DB_PASSWORD=wisecard
DB_PORT=3306
REDIS_HOST=172.17.0.1
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=1234
REDIS_PASSWORD=
Comment on lines +114 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

프로덕션 환경에서 Redis 비밀번호가 비어있습니다.

배포 워크플로우에서 Redis 비밀번호를 빈 값으로 설정하는 것은 보안 위험입니다.

  1. GitHub Secrets에 REDIS_PASSWORD를 추가하세요.
  2. 해당 시크릿을 환경 변수로 전달하세요.

다음과 같이 수정하세요:

           DB_HOST=${{ secrets.DB_HOST }}
           DB_NAME=db-wisecard
           DB_USERNAME=admin
           DB_PASSWORD=${{ secrets.DB_PASSWORD }}
           DB_PORT=3306
           REDIS_HOST=redis
           REDIS_PORT=6379
-          REDIS_PASSWORD=
+          REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
           APP_API_KAKAO_KEY=${{ secrets.APP_API_KAKAO_KEY }}
           APP_PORT=8080
           GRPC_PORT=9091
           JWT_SECRET=${{ secrets.JWT_SECRET }}

그리고 GitHub repository settings에서 REDIS_PASSWORD 시크릿을 추가해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=1234
REDIS_PASSWORD=
DB_HOST=${{ secrets.DB_HOST }}
DB_NAME=db-wisecard
DB_USERNAME=admin
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
DB_PORT=3306
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
APP_API_KAKAO_KEY=${{ secrets.APP_API_KAKAO_KEY }}
APP_PORT=8080
GRPC_PORT=9091
JWT_SECRET=${{ secrets.JWT_SECRET }}
🤖 Prompt for AI Agents
.github/workflows/deploy.yml around lines 114-116: Redis password is currently
set to an empty string which is insecure for production; add a REDIS_PASSWORD
secret in the GitHub repository settings and update the workflow to read the
secret into the environment variable (use secrets.REDIS_PASSWORD) instead of
hardcoding an empty value, ensuring the workflow injects the secret at runtime.

Comment on lines +112 to +116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

환경 변수에 비밀번호를 하드코딩하지 마세요.

DB_PASSWORD를 평문 wisecard로 고정하면 워크플로 로그·PR에서 그대로 노출되어 실서비스 DB가 즉시 위험해집니다. 기존처럼 GitHub Secrets (secrets.DB_PASSWORD)을 사용하도록 되돌려 주세요. Redis 비밀번호도 동일하게 Secrets로 전달돼야 합니다.

-          DB_PASSWORD=wisecard
+          DB_PASSWORD=${{ secrets.DB_PASSWORD }}
...
-          REDIS_PASSWORD=
+          REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DB_PASSWORD=wisecard
DB_PORT=3306
REDIS_HOST=172.17.0.1
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=1234
REDIS_PASSWORD=
DB_PASSWORD=${{ secrets.DB_PASSWORD }}
DB_PORT=3306
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }}
🤖 Prompt for AI Agents
.github/workflows/deploy.yml lines 112-116: DB/Redis passwords are hardcoded
(DB_PASSWORD=wisecard, REDIS_PASSWORD=) — revert to using GitHub Secrets.
Replace the hardcoded DB_PASSWORD and REDIS_PASSWORD with the corresponding
secrets (e.g. set DB_PASSWORD: ${{ secrets.DB_PASSWORD }} and REDIS_PASSWORD:
${{ secrets.REDIS_PASSWORD }}) in the workflow env or job step, remove
plain-text credentials from the file, and ensure the repository Actions secrets
are populated before merging.

APP_API_KAKAO_KEY=${{ secrets.APP_API_KAKAO_KEY }}
APP_PORT=8080
GRPC_PORT=9091
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ WORKDIR /app
COPY --from=builder /app/build/libs/*.jar app.jar

# 포트 노출 (Spring Boot 기본 포트 + gRPC 포트)
EXPOSE 8080 9091
EXPOSE 8080 9090

# 헬스체크 추가
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1

# 애플리케이션 실행
ENTRYPOINT ["java", "-jar", "app.jar"]
ENTRYPOINT ["java", "-jar", "app.jar"]
4 changes: 3 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'

implementation 'me.paulschwarz:spring-dotenv:4.0.0' // .env 읽기
// implementation 'me.paulschwarz:spring-dotenv:4.0.0' // .env 읽기

implementation 'io.github.cdimascio:dotenv-java:2.2.0' // 최신 안정화 버전으로 변경하세요
Comment on lines +61 to +63
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

dotenv 라이브러리 전환이 불완전할 수 있습니다.

spring-dotenvdotenv-java로의 의존성 전환은 단순한 라이브러리 교체가 아니라 코드 레벨의 초기화 방식 변경이 필요합니다. Spring Boot의 자동 설정을 지원하는 spring-dotenv와 달리, dotenv-java는 수동 초기화가 필요합니다.

추가로 다음 사항을 확인해주세요:

  1. 코드 초기화: dotenv-java의 초기화 로직이 적절히 구현되었는지 확인 (예: DotEnv.load() 호출)
  2. 마이그레이션 완성도: spring-dotenv에 대한 모든 참조가 제거되었는지 확인
  3. 라인 63의 주석: "최신 안정화 버전으로 변경하세요"라는 코멘트가 여전히 남아있는데, 이것이 임시 상태를 나타내는 것인지 확인

🏁 Script executed:

#!/bin/bash
# Description: Verify dotenv-java initialization and check for remaining spring-dotenv references

# 1. Search for dotenv-java initialization code
echo "=== Searching for dotenv-java initialization ==="
rg -nC3 'DotEnv\s*\.' --type=java

# 2. Check for any remaining spring-dotenv references
echo "=== Checking for remaining spring-dotenv references ==="
rg -nC3 'spring-dotenv|paulschwarz' --type=java --type=gradle

# 3. Search for @EnableDotEnv or similar Spring-specific annotations
echo "=== Checking for Spring-specific dotenv configurations ==="
rg -nC3 '@EnableDotEnv|spring\.dotenv' --type=java

Length of output: 576


초기 검증 결과, 더 깊은 분석이 필요합니다. 추가 정보를 수집하겠습니다.

추가 검증을 위해 설정 파일과 Application 클래스를 확인하겠습니다.


---

🏁 Script executed:

```shell
#!/bin/bash
# Search for configuration files and Application initialization

# 1. Find and check for application.properties or application.yml
echo "=== Looking for application.properties and application.yml ==="
fd -e properties -e yml | head -20

# 2. Search for Application class and @SpringBootApplication
echo "=== Searching for Application class ==="
rg -nC5 '@SpringBootApplication|@SpringBootTest' --type=java

# 3. Search for @Configuration beans
echo "=== Searching for @Configuration and @Bean ==="
rg -nC3 '@Configuration|@Bean' --type=java | head -50

# 4. Check if there's any initialization or PostConstruct
echo "=== Searching for @PostConstruct or initialization ==="
rg -nC3 '@PostConstruct|implements.*InitializingBean' --type=java

Length of output: 9971


dotenv-java 마이그레이션이 불완전합니다. 즉시 수정이 필요합니다.

검증 결과 다음 문제들을 발견했습니다:

  1. dotenv-java 초기화 코드 없음 (CRITICAL): 코드베이스에서 Dotenv.load() 또는 Dotenv.configure().load() 호출이 전혀 없습니다. dotenv-java는 spring-dotenv와 달리 수동으로 초기화해야 하므로, 현재 상태에서는 .env 파일이 로드되지 않습니다.

  2. 버전 문제 (MAJOR): build.gradle의 dotenv-java는 2.2.0인데, 이는 구버전입니다(2023년 1월). 라인 63의 주석 "최신 안정화 버전으로 변경하세요"는 실제로 실현되지 않았습니다.

  3. 라인 63 주석 제거: "최신 안정화 버전으로 변경하세요"라는 한국어 주석은 코드에서 제거해야 합니다.

필수 조치:

  • DemoApplication.java 또는 별도의 @Configuration 클래스에서 Dotenv.load() 초기화 코드 추가
  • dotenv-java 버전을 최신 버전으로 업그레이드 검토
  • 라인 63의 주석 제거
🤖 Prompt for AI Agents
In build.gradle around lines 61 to 63: the dotenv-java migration is incomplete —
the dependency is pinned to an old 2.2.0 and a Korean comment on line 63 should
be removed; add explicit initialization of dotenv (e.g., call Dotenv.load() or
Dotenv.configure().load()) in your application startup (DemoApplication.java
main method or a dedicated @Configuration bean) so .env is loaded at startup,
update the build.gradle dotenv-java dependency to the latest stable release, and
remove the line 63 comment "최신 안정화 버전으로 변경하세요".


// JWT
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
Expand Down
31 changes: 10 additions & 21 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@

services:
# Redis는 AWS ElastiCache를 사용하므로 로컬 컨테이너 제거
# Redis 컨테이너 추가
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
networks:
- wisecard-network
Comment on lines +3 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redis 서비스 구성에 보안 및 데이터 지속성 문제가 있습니다.

Redis 서비스를 추가한 것은 좋지만 다음 문제들을 해결해야 합니다:

  1. 보안: Redis에 비밀번호가 설정되어 있지 않습니다. 내부 네트워크라도 최소한의 인증을 설정하는 것이 좋습니다.
  2. 데이터 지속성: 볼륨이 마운트되어 있지 않아 컨테이너 재시작 시 모든 데이터가 손실됩니다.
  3. 리소스 제한: 메모리 및 CPU 제한이 없어 시스템 리소스를 과도하게 사용할 수 있습니다.

다음과 같이 개선하는 것을 권장합니다:

   redis:
     image: redis:alpine
     container_name: redis
     restart: unless-stopped
+    command: redis-server --requirepass ${REDIS_PASSWORD}
+    volumes:
+      - redis_data:/data
+    deploy:
+      resources:
+        limits:
+          memory: 512M
+          cpus: '0.5'
     networks:
       - wisecard-network

그리고 volumes 섹션에 Redis 데이터 볼륨 추가:

 volumes:
   app_logs:
     driver: local
+  redis_data:
+    driver: local

또한, .env 파일이나 GitHub Secrets에 REDIS_PASSWORD를 설정하고 앱 서비스의 환경 변수에도 동일한 비밀번호를 전달해야 합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
networks:
- wisecard-network
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
deploy:
resources:
limits:
memory: 512M
cpus: '0.5'
networks:
- wisecard-network
Suggested change
redis:
image: redis:alpine
container_name: redis
restart: unless-stopped
networks:
- wisecard-network
volumes:
app_logs:
driver: local
redis_data:
driver: local


# WiseCard Backend 애플리케이션
app:
Expand All @@ -24,39 +29,23 @@ services:
JWT_SECRET: ${JWT_SECRET:-}
ports:
- "${APP_PORT:-8080}:8080"
- "${GRPC_PORT:-9091}:9091"
- "${GRPC_PORT:-9090}:9090"
# volumes:
# - app_logs:/app/logs
networks:
- wisecard-network
# Redis는 AWS ElastiCache를 사용하므로 의존성 제거
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
test: ["CMD", "curl", "-f", "http://localhost:8080/api/health/check"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

# Nginx 리버스 프록시 (선택사항)
# nginx:
# image: nginx:alpine
# container_name: wisecard-nginx
# restart: unless-stopped
# ports:
# - "80:80"
# - "443:443"
# volumes:
# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
# - ./nginx/ssl:/etc/nginx/ssl:ro
# networks:
# - wisecard-network
# depends_on:
# - app

volumes:
app_logs:
driver: local

networks:
wisecard-network:
driver: bridge
driver: bridge
12 changes: 12 additions & 0 deletions gRPC/src/main/proto/cardCompany.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto3";

package com.sub.grpc;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

패키지 경로 불일치로 Buf 빌드 실패

이 파일이 gRPC/src/main/proto에 그대로 위치한 상태에서 package com.sub.grpc;를 선언해 Buf 규칙 PACKAGE_DIRECTORY_MATCH가 위반되고 있습니다. 현재 구조로는 Buf/protoc 빌드가 실패하므로, 파일을 gRPC/src/main/proto/com/sub/grpc/cardCompany.proto로 이동하거나 패키지 선언을 경로와 일치하도록 조정해 주세요.

🧰 Tools
🪛 Buf (1.58.0)

3-3: Files with package "com.sub.grpc" must be within a directory "com/sub/grpc" relative to root but were in directory "gRPC/src/main/proto".

(PACKAGE_DIRECTORY_MATCH)

🤖 Prompt for AI Agents
gRPC/src/main/proto/cardCompany.proto around lines 3 to 3: 패키지 선언과 파일 경로가 일치하지
않아 Buf 규칙 PACKAGE_DIRECTORY_MATCH가 위반됩니다; 해결 방법은 두 가지 중 하나를 택해 적용하세요 — 파일을
gRPC/src/main/proto/com/sub/grpc/cardCompany.proto로 이동하여 디렉토리 구조가 package
com.sub.grpc; 선언과 일치하도록 하거나, 현재 파일 위치를 유지하려면 package 선언을 파일 경로에 맞게 변경(예: package
com.sub; 또는 적절한 경로 네임스페이스로 수정)한 후 Buf/protoc 빌드를 다시 실행해 오류가 사라지는지 확인하세요.


enum CardCompany {
HANA = 0;
HYUNDAI = 1;
KOOKMIN = 2;
LOTTE = 3;
SAMSUNG = 4;
SHINHAN = 5;
}
88 changes: 48 additions & 40 deletions gRPC/src/main/proto/cardData.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,72 @@ syntax = "proto3";

package com.sub.grpc;

// 온라인/오프라인/BOTH 구분
import "google/protobuf/empty.proto";
import "cardCompany.proto";

Comment on lines +5 to +7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

존재하지 않는 proto 파일을 임포트하고 있습니다.

정적 분석에서 cardCompany.proto 파일을 찾지 못해 컴파일이 중단됩니다. 실제 파일 경로나 파일명을 맞춰 수정하거나, 필요한 경우 버전 관리에 파일을 추가해 주세요.

🧰 Tools
🪛 Buf (1.58.0)

6-6: import "cardCompany.proto": file does not exist

(COMPILE)

🤖 Prompt for AI Agents
In gRPC/src/main/proto/cardData.proto around lines 5 to 7, the file is importing
a non‑existent proto ("cardCompany.proto") which breaks compilation; fix this by
either correcting the import path/filename to the actual proto (e.g., adjust
relative path or correct casing) or add the missing cardCompany.proto to version
control at the referenced location, then update any import statements and re-run
the proto compiler to verify resolution.

enum ChannelType {
ONLINE = 0;
OFFLINE = 1;
BOTH = 2;
}

// 할인 혜택
enum CardType {
CREDIT = 0;
DEBIT = 1;
}

message DiscountBenefit {
int64 id = 6; // 변경 식별용 id 추가
double rate = 1; // 정률 할인 (0이면 null)
double amount = 2; // 정액 할인 (0이면 null)
int64 minimum_amount = 3; // 최소 결제 금액 (0이면 null)
int64 benefit_limit = 4; // 최대 혜택 한도 (0이면 null)
ChannelType channel = 5; // 온라인/오프라인/BOTH
double rate = 1; // 정률 할인
int32 amount = 2; // 정액 할인
int32 minimum_amount = 3; // 최소 결제 금액
int32 benefit_limit = 4; // 최대 혜택 한도
int32 minimum_spending = 5; // 전월 최소 실적 (원 단위)
ChannelType channel = 6; // 온라인/오프라인/BOTH
}

// 포인트 적립 혜택
message PointBenefit {
int64 id = 6; // 변경 식별용 id 추가
double rate = 1; // 적립률 (0이면 null)
int64 minimum_amount = 2; // 최소 결제 금액 (0이면 null)
int64 benefit_limit = 3; // 최대 혜택 한도 (0이면 null)
ChannelType channel = 4; // 온라인/오프라인/BOTH
string name = 1; //포인트 이름
int32 amount = 2; //적립금
double rate = 3; // 적립률
int32 minimum_amount = 4; // 최소 결제 금액
int32 benefit_limit = 5; // 최대 혜택 한도
int32 minimum_spending = 6; // 전월 최소 실적 (원 단위)
ChannelType channel = 7; // 온라인/오프라인/BOTH
}

// 캐시백 혜택
message CashbackBenefit {
int64 id = 6; // 변경 식별용 id 추가
double rate = 1; // 캐시백률 (0이면 null)
double amount = 2; // 정액 캐시백 (0이면 null)
int64 minimum_amount = 3; // 최소 결제 금액 (0이면 null)
int64 benefit_limit = 4; // 최대 혜택 한도 (0이면 null)
ChannelType channel = 5; // 온라인/오프라인/BOTH
double rate = 1; // 캐시백률
int32 amount = 2; // 정액 캐시백
int32 minimum_amount = 3; // 최소 결제 금액
int32 benefit_limit = 4; // 최대 혜택 한도
int32 minimum_spending = 5; // 전월 최소 실적 (원 단위)
ChannelType channel = 6; // 온라인/오프라인/BOTH
}

// 카드 단위
message Benefit {
int64 benefit_id = 1; // 혜택 고유 ID (기존 혜택 식별용) -> 추가한 것
repeated DiscountBenefit discounts = 2; // 할인 혜택 배열
repeated PointBenefit points = 3; // 포인트 혜택 배열
repeated CashbackBenefit cashbacks = 4; // 캐시백 혜택 배열
repeated string applicable_category = 5; // 카카오맵 업종 코드 (MT1, FD6, CE7 ...)
repeated string applicable_targets = 6; // 브랜드명 또는 한글 설명
repeated DiscountBenefit discounts = 1; // 할인 혜택 배열
repeated PointBenefit points = 2; // 포인트 혜택 배열
repeated CashbackBenefit cashbacks = 3; // 캐시백 혜택 배열
repeated string categories = 4; // 카카오맵 업종 코드 (MT1, FD6, CE7 ...)
repeated string targets = 5; // 브랜드명 또는 한글 설명
string summary = 6; //한줄 요약
}

message CardBenefit {
int32 card_id = 1; // 카드 id
CardCompany card_company = 2; // 카드사
string card_name = 3; //카드명
string img_url = 4; //카드 이미지
CardType card_type = 5; //CREDIT|DEBIT
repeated Benefit benefits = 6; // 카드에 속한 여러 혜택
}

message CrawledBenefit {
int64 card_id = 6; // 변경 식별용 id 추가
string card_bank = 1; // 카드사
string card_name = 2; //카드명
string img_url = 3; //카드 이미지
string type = 4; //CreditCard|DebitCard
repeated Benefit benefits = 5; // 카드에 속한 여러 혜택
message CardBenefitList {
repeated CardBenefit cardBenefits = 1;
}

// 카드 여러 장 처리
message CrawledBenefitList {
repeated CrawledBenefit crawledBenefit = 1;
service CardService {
rpc receiveCardBenefits(CardBenefitList) returns (google.protobuf.Empty);
}

// 응답 메시지
Expand All @@ -69,7 +77,7 @@ message CardSaveResponse {
int32 saved_count = 3;
}

// gRPC 서비스 정의
// API 서버 저장 서비스
service CardDataService {
rpc SaveCardData(CrawledBenefitList) returns (CardSaveResponse);
rpc SaveCardData(CardBenefitList) returns (CardSaveResponse);
}
37 changes: 37 additions & 0 deletions gRPC/src/main/proto/promotion.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
syntax = "proto3";

package com.sub.grpc;


import "google/protobuf/empty.proto";
import "google/protobuf/timestamp.proto";
import "cardCompany.proto";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

cardCompany.proto import 해석 실패

Buf가 import "cardCompany.proto";에서 “file does not exist” 오류를 보고하고 있습니다. 앞선 cardCompany.proto가 패키지 경로와 맞지 않게 배치되어 있어 탐색에 실패하는 것으로 보이며, 이 상태에서는 gRPC 코드 생성이 진행되지 않습니다. cardCompany.proto를 패키지 구조에 맞게 이동시키거나 올바른 경로로 import를 수정해 빌드가 통과하도록 조치해 주세요.

🧰 Tools
🪛 Buf (1.58.0)

8-8: import "cardCompany.proto": file does not exist

(COMPILE)

🤖 Prompt for AI Agents
gRPC/src/main/proto/promotion.proto around line 8: the import statement
references "cardCompany.proto" but Buf reports "file does not exist" because
cardCompany.proto is not located where the import expects; fix by either moving
cardCompany.proto into the same proto directory or package-relative folder
matching the proto package structure, or update the import to the correct
relative path (for example "path/to/cardCompany.proto") that matches the repo
layout and buf.build configuration; after relocating or editing the import, run
buf lint/build to verify the file is discoverable and code generation completes.


message CardPromotion {
CardCompany card_company = 1; // 카드사
string description = 2; // 프로모션 설명
string img_url = 3; // 프로모션 이미지
string url = 4; // 프로모션 링크
google.protobuf.Timestamp start_date = 5; // 시작 날짜
google.protobuf.Timestamp end_date = 6; // 끝 날짜
}

message CardPromotionList {
repeated CardPromotion cardPromotion = 1;
}

service PromotionService {
rpc receivePromotions(CardPromotionList) returns (google.protobuf.Empty);
}

// 응답 메시지
message PromotionSaveResponse {
bool success = 1;
string message = 2;
int32 saved_count = 3;
}

// API 서버 저장 서비스
service CardPromotionService{
rpc SavedPromotions(CardPromotionList) returns (PromotionSaveResponse);
}
6 changes: 3 additions & 3 deletions gRPC/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
grpc:
client:
local-grpc-server: # ????? ??
address: "static://localhost:9090" # ?? ??(??? ??? ?? ??IP)
local-grpc-server:
address: "static://localhost:9090"
negotiationType: "plaintext"

server:
port: 50051
port: 9092
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long
Optional<RefreshToken> findByMemberId(Long memberId);

Boolean existsByToken(String token);

void deleteByMemberId(Long memberId);
}
37 changes: 11 additions & 26 deletions src/main/java/com/example/demo/auth/service/KakaoLoginService.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.example.demo.auth.entity.Member;
import com.example.demo.auth.jwt.JwtTokenProvider;
import com.example.demo.auth.repository.MemberRepository;
import com.example.demo.auth.repository.RefreshTokenRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -23,35 +24,19 @@ public class KakaoLoginService {
private final KakaoOAuthClient kakaoOAuthClient;
private final JwtTokenProvider jwtTokenProvider;
private final RefreshTokenService refreshTokenService;

@Transactional
public TokenResponse signup(AccessTokenRequest request) {
KakaoUserInfo kakaoUserInfo = kakaoOAuthClient.retrieveUserInfo(request.accessToken());

if (memberRepository.findBySocialId(kakaoUserInfo.getId()).isPresent()) {
throw new RuntimeException("이미 가입된 사용자입니다.");
}

Member member = Member.builder()
.socialId(kakaoUserInfo.getId())
.name(kakaoUserInfo.getNickName())
.email(kakaoUserInfo.getEmail())
.build();

Member newMember = memberRepository.save(member);

String accessToken = jwtTokenProvider.createAccessToken(newMember.getId());
String refreshToken = jwtTokenProvider.createRefreshToken(newMember.getId());

refreshTokenService.save(newMember.getId(), refreshToken);

return new TokenResponse(accessToken, refreshToken);
}
private final RefreshTokenRepository refreshTokenRepository;

@Transactional
public TokenResponse login(AccessTokenRequest request) {
KakaoUserInfo kakaoUserInfo = kakaoOAuthClient.retrieveUserInfo(request.accessToken());
Member member = memberRepository.findBySocialId(kakaoUserInfo.getId()).orElseThrow();
Member member = memberRepository.findBySocialId(kakaoUserInfo.getId()).orElseGet(() -> {
Member newMember = Member.builder()
.socialId(kakaoUserInfo.getId())
.name(kakaoUserInfo.getNickName())
.email(kakaoUserInfo.getEmail())
.build();
Comment on lines +33 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

카카오 사용자 정보 null 검증 필요

kakaoUserInfo.getNickName() 또는 getEmail()이 null일 가능성을 고려해야 합니다. 카카오 API는 사용자가 정보 제공에 동의하지 않은 경우 null을 반환할 수 있습니다.

Member 생성 전 필수 필드 검증을 추가하세요:

 Member newMember = Member.builder()
         .socialId(kakaoUserInfo.getId())
-        .name(kakaoUserInfo.getNickName())
-        .email(kakaoUserInfo.getEmail())
+        .name(Optional.ofNullable(kakaoUserInfo.getNickName()).orElse("Unknown"))
+        .email(Optional.ofNullable(kakaoUserInfo.getEmail()).orElse(""))
         .build();

또는 필수 정보가 없을 경우 명시적으로 예외를 발생시키세요:

+if (kakaoUserInfo.getNickName() == null) {
+    throw new IllegalArgumentException("카카오 닉네임 정보가 필요합니다");
+}
 Member newMember = Member.builder()
         .socialId(kakaoUserInfo.getId())
         .name(kakaoUserInfo.getNickName())
         .email(kakaoUserInfo.getEmail())
         .build();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Member newMember = Member.builder()
.socialId(kakaoUserInfo.getId())
.name(kakaoUserInfo.getNickName())
.email(kakaoUserInfo.getEmail())
.build();
Member newMember = Member.builder()
.socialId(kakaoUserInfo.getId())
.name(Optional.ofNullable(kakaoUserInfo.getNickName()).orElse("Unknown"))
.email(Optional.ofNullable(kakaoUserInfo.getEmail()).orElse(""))
.build();

return memberRepository.save(newMember);
});
Comment on lines +32 to +39
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

동시 로그인 시 레이스 컨디션 위험

동일한 socialId로 동시에 여러 로그인 요청이 들어올 경우 레이스 컨디션이 발생할 수 있습니다:

  1. 두 요청 모두 findBySocialId()에서 empty를 반환
  2. 두 요청 모두 새 Member 생성 시도
  3. socialId에 유니크 제약조건이 있다면 하나는 실패, 없다면 중복 회원 생성

다음 중 하나의 해결책을 적용하세요:

해결책 1: 데이터베이스 제약조건 + 예외 처리

Member.socialId에 유니크 제약조건을 추가하고 재시도 로직 구현:

 @Transactional
 public TokenResponse login(AccessTokenRequest request) {
     KakaoUserInfo kakaoUserInfo = kakaoOAuthClient.retrieveUserInfo(request.accessToken());
-    Member member = memberRepository.findBySocialId(kakaoUserInfo.getId()).orElseGet(() -> {
-        Member newMember = Member.builder()
-                .socialId(kakaoUserInfo.getId())
-                .name(kakaoUserInfo.getNickName())
-                .email(kakaoUserInfo.getEmail())
-                .build();
-        return memberRepository.save(newMember);
-    });
+    Member member = findOrCreateMember(kakaoUserInfo);
     
     String accessToken = jwtTokenProvider.createAccessToken(member.getId());
     String refreshToken = jwtTokenProvider.createRefreshToken(member.getId());
     
     refreshTokenService.saveOrUpdateToken(member.getId(), refreshToken);
     
     return new TokenResponse(accessToken, refreshToken);
 }
+
+private Member findOrCreateMember(KakaoUserInfo kakaoUserInfo) {
+    return memberRepository.findBySocialId(kakaoUserInfo.getId())
+            .orElseGet(() -> {
+                try {
+                    Member newMember = Member.builder()
+                            .socialId(kakaoUserInfo.getId())
+                            .name(kakaoUserInfo.getNickName())
+                            .email(kakaoUserInfo.getEmail())
+                            .build();
+                    return memberRepository.save(newMember);
+                } catch (DataIntegrityViolationException e) {
+                    // 동시 생성 시도로 인한 제약조건 위반 - 재조회
+                    return memberRepository.findBySocialId(kakaoUserInfo.getId())
+                            .orElseThrow(() -> new IllegalStateException("Member creation failed"));
+                }
+            });
+}

해결책 2: 비관적 락 사용

별도의 락 메커니즘을 통해 동시 생성 방지:

@Transactional
public TokenResponse login(AccessTokenRequest request) {
    KakaoUserInfo kakaoUserInfo = kakaoOAuthClient.retrieveUserInfo(request.accessToken());
    
    synchronized (("MEMBER_LOCK_" + kakaoUserInfo.getId()).intern()) {
        Member member = memberRepository.findBySocialId(kakaoUserInfo.getId())
                .orElseGet(() -> {
                    Member newMember = Member.builder()
                            .socialId(kakaoUserInfo.getId())
                            .name(kakaoUserInfo.getNickName())
                            .email(kakaoUserInfo.getEmail())
                            .build();
                    return memberRepository.save(newMember);
                });
        
        String accessToken = jwtTokenProvider.createAccessToken(member.getId());
        String refreshToken = jwtTokenProvider.createRefreshToken(member.getId());
        
        refreshTokenService.saveOrUpdateToken(member.getId(), refreshToken);
        
        return new TokenResponse(accessToken, refreshToken);
    }
}
🤖 Prompt for AI Agents
In src/main/java/com/example/demo/auth/service/KakaoLoginService.java around
lines 32-39 there is a race condition when creating a Member for a given
socialId; fix by applying one of the two options: (A) Add a unique DB constraint
on Member.socialId and change the creation flow to try findBySocialId(), if
absent attempt save() inside a try/catch that catches the unique-constraint
exception (e.g., DataIntegrityViolationException), and on exception re-query
findBySocialId() to return the existing record (optionally retry a couple
times); or (B) prevent concurrent creation with a lock around the create path
(e.g., use a database-level pessimistic lock by selecting for update on a
parent/lookup row or use a JVM-level per-socialId synchronized/lock (interned
key) within a @Transactional method) so only one thread can create the Member;
implement one of these and remove the current plain orElseGet creation to ensure
duplicate members cannot be inserted.


String accessToken = jwtTokenProvider.createAccessToken(member.getId());
String refreshToken = jwtTokenProvider.createRefreshToken(member.getId());
Expand All @@ -66,10 +51,10 @@ public TokenResponse reissue(RefreshTokenRequest request) {
return jwtTokenProvider.reissueToken(request.refreshToken());
}


@Transactional
public void withdraw() {
Long memberId = getMemberId();
refreshTokenRepository.deleteByMemberId(memberId);
memberRepository.deleteById(memberId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record CashbackBenefitDTO(
double amount,
long minimumAmount,
long benefitLimit,
ChannelType channel
ChannelType channel,
Integer minimumSpending
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public record DiscountBenefitDTO(
double amount,
double minimumAmount,
double benefitLimit,
ChannelType channel
ChannelType channel,
Integer minimumSpending
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ public record PointBenefitDTO(
double rate,
long minimumAmount,
long benefitLimit,
ChannelType channel
ChannelType channel,
Integer minimumSpending,
String name
){
}
Loading