chore(dev): W3-2 테스트용 dev seed + serena memory
리뷰/댓글 기능 수동 테스트를 위한 멱등 dev seed. - seed-dev.sql / seed-dev-teardown.sql: 로그인 테스트 계정 1, 더미 게임 1, 별점 다양한 더미 리뷰 5(유저별 1리뷰 unique 충족), 댓글 3. NOT EXISTS 가드로 재실행 안전. - serena memory: PBKDF2(BCrypt 아님) 해시 형식, dev-seed blueprint(FK순서+active-unique), JDK 컨테이너 교차검증 패턴 기록. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019tHAb6XYHWDPzb82FNKugo
This commit is contained in:
parent
e98c437765
commit
3a06b39d76
|
|
@ -0,0 +1,224 @@
|
|||
---
|
||||
schema_version: 2
|
||||
sid: 20260618-121419
|
||||
started_at: 2026-06-18T12:14:19+09:00
|
||||
ended_at: 2026-06-18T13:05:00+09:00
|
||||
applied_changes:
|
||||
- "Serena memory 기록: bibimbap-auth-pbkdf2-not-bcrypt (수용, signal=observation)"
|
||||
- "Serena memory 기록: bibimbap-dev-seed-blueprint (수용, FK순서+active-unique 통합)"
|
||||
- "Serena memory 기록: java-crypto-verify-via-jdk-container-when-no-host-jdk (수용, signal=positive)"
|
||||
- "docs_sync 보류: CLAUDE.md / verification-strategies.md 반영은 tracked 문서 편집+미커밋이라 사용자 승인 대기"
|
||||
user_request: |
|
||||
새로 추가한 기능(게임 리뷰/댓글, W3-2) 테스트하기 위한 더미 상황이 필요하다:
|
||||
1. 게임 (dummy game)
|
||||
2. 계정 (dummy account, 로그인 가능)
|
||||
3. 이미 등록되어 있는 더미 리뷰
|
||||
status: done
|
||||
---
|
||||
|
||||
# Summary
|
||||
W3-2 게임 리뷰/댓글 기능 테스트용 더미 상황을 dev 스키마에 구축. 멱등 seed SQL
|
||||
(`db/seed-dev.sql`) + 정리 SQL(`db/seed-dev-teardown.sql`) 작성 후 가동중 컨테이너
|
||||
(bibimbap-db)에 적용. 구성: 로그인 가능 테스트 계정 1개(tester@bibimbap.local /
|
||||
test1234!), 표시되는 더미 게임 1개(테스트 계정 소유), 별점 다양한 더미 리뷰 5개
|
||||
(각자 다른 더미 유저 — game당 user 1리뷰 unique 제약 충족), 보너스 댓글 3개.
|
||||
로그인 계정은 해당 게임에 리뷰가 없어 "직접 작성" 테스트도 가능.
|
||||
|
||||
핵심 제약 발견: 로그인 password_hash 는 BCrypt 가 아닌 자체 PBKDF2
|
||||
(`pbkdf2_sha256$210000$<saltB64>$<hashB64>`, PBKDF2WithHmacSHA256/256bit/16B salt).
|
||||
저장 해시가 실제 Java `UserController.verifyPassword` 경로로 검증됨을 JDK 컨테이너에서
|
||||
교차 실행 확인(verify('test1234!')=true, 오답=false) → 로그인 동작 보장.
|
||||
|
||||
# Invocations
|
||||
- research-advisor: 인증 seed 경로 + 리뷰/댓글 유저귀속 + seed 적용경로 조사 (완료)
|
||||
- artifact: research/index.md (file:line 근거 포함). 결론 = orchestrator 독립 조사와 일치.
|
||||
- graph-refresh-checker: seed SQL 추가 후 graph staleness 판정 (running)
|
||||
|
||||
# Decisions
|
||||
- 적용 방식: 재사용 SQL 파일(db/seed-dev.sql) + 즉시 적용. (사용자 선택, 추천값)
|
||||
- 로그인 자격: 기본값 tester@bibimbap.local / test1234! / 닉네임 '테스터'. (사용자 선택)
|
||||
- 규모: 게임 1 + 리뷰 5(별점 5/4/3/5/2). (사용자 선택)
|
||||
- 설계: 리뷰어는 별도 더미 유저 5명(unique 제약 game당 user 1리뷰 때문). 로그인 계정은
|
||||
그 게임에 리뷰 없음(작성 테스트 가능). 게임 소유자 = 로그인 계정(getGamesByUserId 도 테스트).
|
||||
- design/implementation-advisor 스킵: 스키마·계약·컬럼값 완비된 단일 seed SQL → orchestrator 직접 구현.
|
||||
- 댓글 3개 보너스 포함: 신규 기능이 리뷰/댓글 동시 배치라 같은 더미 상황에 포함이 합리적.
|
||||
|
||||
# Advisor Invocation Decision Log
|
||||
- advisor: requirements-advisor
|
||||
decision: skip
|
||||
rationale: '요청 3항목 명확, seed 위치/자격/규모는 orchestrator 가 plan-gate(AskUserQuestion)로 직접 수렴'
|
||||
checked_at: 2026-06-18T12:14:30+09:00
|
||||
- advisor: research-advisor
|
||||
decision: call
|
||||
rationale: '로그인 가능 계정 seed 위한 password_hash 알고리즘·provider·세션귀속이 미문서화 미지수'
|
||||
checked_at: 2026-06-18T12:15:00+09:00
|
||||
- advisor: design-advisor
|
||||
decision: skip
|
||||
rationale: 'research 로 컬럼·계약·제약 전부 확정 → 설계 오픈질문 0, orchestrator 직접 설계'
|
||||
checked_at: 2026-06-18T12:30:00+09:00
|
||||
- advisor: implementation-advisor
|
||||
decision: skip
|
||||
rationale: '단일 SQL 파일 산출, 파일 충돌 없음 → worker 분산 불요'
|
||||
checked_at: 2026-06-18T12:31:00+09:00
|
||||
- advisor: verification-advisor
|
||||
decision: skip-direct
|
||||
rationale: 'orchestrator 가 매퍼 동등쿼리 + JDK 교차해시검증 + 멱등 재실행을 직접 실행해 AC 충족 확인'
|
||||
checked_at: 2026-06-18T12:45:00+09:00
|
||||
|
||||
# user_signals
|
||||
positive: []
|
||||
negative: []
|
||||
|
||||
# verified_by_me
|
||||
- L1 (데이터 무결성 / 매퍼 동등 쿼리, 실 DB 실행):
|
||||
- login identity 조회(provider=email): tester 1건 + password_hash 존재 → pass
|
||||
- tester users.status=ACTIVE (로그인 필수 조건) → pass
|
||||
- getVisibleGames 동등: '테스트 게임 (더미)' is_visible=true 포함 → pass
|
||||
- listGameReviews(game): 5건, 최신순 정렬 + authorName JOIN 정상 → pass
|
||||
- getActiveReviewByGameAndUser(game, tester): 0건 → 본인 리뷰 작성 테스트 가능 → pass
|
||||
- listGameComments(game): 3건 → pass
|
||||
- L2 (교차구현 해시 검증, 실제 Java verifyPassword 코드 경로, eclipse-temurin:21-jdk):
|
||||
- verify('test1234!', 저장해시) = true → 로그인 성공 보장
|
||||
- verify('wrongpass', 저장해시) = false → 음성 케이스 정상
|
||||
- 멱등성: seed-dev.sql 재실행 후 카운트 불변(users6/games1/reviews5/comments3) → pass
|
||||
- 로그 스캔: psql ON_ERROR_STOP=1 무에러, NOTICE 정상 → clean
|
||||
|
||||
# needs_user_verification
|
||||
- 런타임 UI 스모크 1회 (호스트에 JDK 없음 + bibimbap-app 미가동이라 자동 불가):
|
||||
1. `docker compose up --build` (또는 호스트 실행) 으로 앱 기동
|
||||
2. tester@bibimbap.local / test1234! 로 로그인 → 성공 확인
|
||||
3. '테스트 게임 (더미)' 상세 진입 → 기존 리뷰 5건 + 댓글 3건 표시 확인
|
||||
4. 로그인 상태로 리뷰 1건 + 댓글 1건 신규 작성 → 정상 등록 확인
|
||||
|
||||
# graph_refresh
|
||||
- 판정: partial-stale (but 본 세션 변경은 fresh).
|
||||
- 본 세션 변경(db/seed-dev.sql, seed-dev-teardown.sql)은 src/docs graph scope 대상경로
|
||||
밖 데이터 픽스처 → graph 영향 0, 재생성 불요(no-defer: 본 세션 무액션).
|
||||
- partial-stale 트리거는 **별도 작업단위**(W3-2 댓글/리뷰 기능 구현 — GameCommentController/
|
||||
GameReviewController/GameReviewsMapper/GameReviewData 등, 대부분 untracked). 해당 코드가
|
||||
미커밋 진행중이라 지금 /graphify src/ 재생성은 시기상조 → open_items 로 권고만 이월하지
|
||||
않고 "기능 커밋 시점 처리" 로 명시.
|
||||
|
||||
# post-session-debug (로그인 server error 후속)
|
||||
- 사용자 보고 "로그인 시 server error(500)". 진단 결과:
|
||||
- 원인 = **stale 앱 인스턴스**(pid 59109, 6/17 14:36 기동, 최근 auth 수정 이전 빌드).
|
||||
내 seed 가 tester 계정을 만들어 처음으로 "계정 존재" 상태 로그인 경로를 타며 표면화.
|
||||
- 조치: 사용자 동의(직접 캡처 요청) 하에 pid 59109 종료 → `JAVA_HOME=/opt/homebrew/opt/openjdk@21
|
||||
./mvnw -P dev spring-boot:run` 재기동(현재 코드 = target/classes 재컴파일, W3-2 컨트롤러 포함).
|
||||
로그: /tmp/bibimbap-app.log. 신규 pid 23318 (백그라운드).
|
||||
- 결과: `POST /login` 200, `GET /game/3/reviews` 200(5건), `GET /game/3/comments` 200(3건),
|
||||
`GET /game/3` 200. 더미 상황 런타임 정상. → needs_user_verification 의 UI 스모크 사실상 완료.
|
||||
- **발견 버그 (별도 작업단위 = W3-2 댓글기능, 내 seed 무관)**: 댓글 목록 author 항상 null.
|
||||
- 근거: GameCommentsMapper.listGameComments 가 `nickname AS authorName` 로 alias →
|
||||
결과 컬럼 `authorName` 인데 GameCommentData 엔 `nickname` 프로퍼티만 존재 → 미매핑 →
|
||||
commentView(GameCommentController.java:179) `getNickname()` null.
|
||||
- 대조: 단건 getGameComment 는 `nickname`(alias 없음) → 정상.
|
||||
- 1줄 수정안: listGameComments 의 `nickname AS authorName` → `nickname`.
|
||||
- 처리: 사용자 요청 시에만 (in-progress 기능 코드라 무단 수정 보류).
|
||||
|
||||
# open_items
|
||||
- 미커밋 신규 파일: db/seed-dev.sql, db/seed-dev-teardown.sql (작업단위 산출물).
|
||||
프로젝트/하니스 커밋정책 = 사용자 요청 시에만 커밋 → 커밋 보류, 사용자 확인 대기.
|
||||
- (별도 작업단위) W3-2 댓글/리뷰 기능 코드 커밋 시 `/graphify src/` 재생성 + docs/graph/index.md
|
||||
frontmatter(source_commit, last_generated_at) + Scopes 표 src 행 갱신 권고. 본 더미데이터
|
||||
task 범위 밖.
|
||||
|
||||
# Retrospective
|
||||
Retrospective:
|
||||
signals:
|
||||
positive:
|
||||
- quote_or_paraphrase: "추천 옵션 전부 수락 (seed 적용방식·자격·규모 plan-gate 선택지 그대로 채택)"
|
||||
about: "orchestrator 의 plan-gate(AskUserQuestion) 추천값 + research 선행으로 advisor 다수 스킵한 경량 경로"
|
||||
- quote_or_paraphrase: "재지시·재호출 없이 단일 패스로 수렴 (부정 시그널 0)"
|
||||
about: "research 1회로 컬럼·계약·제약을 전부 확정 → design/impl/verification advisor 스킵한 판단이 검증됨"
|
||||
negative: []
|
||||
what_went_well:
|
||||
- "research-advisor 선행 1회로 인증 메커니즘(PBKDF2)·유저귀속·seed 적용경로를 모두 확정해, design/implementation/verification advisor 3종을 근거 있게 스킵하고 orchestrator 직접 구현으로 수렴. advisor invocation decision log 에 skip rationale 가 항목별로 남아 사후 추적 가능."
|
||||
- "호스트에 JDK 가 없는 제약에도 password_hash 를 추정·방치하지 않고 eclipse-temurin:21-jdk 컨테이너에서 실제 verifyPassword 코드 경로로 교차 실행(positive=true / negative=false)해 '로그인 가능' AC 를 자기검증으로 닫음. self-report 가 아닌 실행 증거."
|
||||
- "schema.sql 이 '비권위 복원본'이라는 메타 한계를 research concerns 에 보존하고, 컬럼명은 매퍼로 교차확인하되 제약·타입은 추정으로 표기해 신뢰도 과대평가를 회피."
|
||||
- "seed SQL 을 멱등 + teardown 쌍으로 작성하고 재실행 후 카운트 불변을 실증해 반복 적용 안전성을 확보."
|
||||
- "graph staleness 를 partial-stale 로 정직하게 판정하되, 본 세션 변경(데이터 픽스처)은 graph scope 밖임을 구분해 불필요한 재생성을 회피하고 잔여 stale 은 별도 작업단위로 정확히 귀속."
|
||||
what_to_improve:
|
||||
- "런타임 UI 스모크는 호스트 환경 제약(JDK 없음 + 앱 미가동)으로 자동 불가 → needs_user_verification 으로 정확히 이월됨. 개선이라기보다 환경 한계의 정직한 핸드오프. (구조적 결함 아님)"
|
||||
- "seed SQL 의 제약/타입/default 는 schema.sql 비권위 유래라 실 DB `\\d` 대조까지는 미수행(행수·테이블존재·매퍼 동등쿼리로만 검증). 운영 DB 와 default 가 다르면 재현 시 불일치 여지 — 현 dev 스키마에선 무영향이나 운영 적용 시 유의."
|
||||
memory_candidates:
|
||||
- name: bibimbap-auth-pbkdf2-not-bcrypt
|
||||
type: reference
|
||||
description: "bibimbap 로그인 해시는 BCrypt 가 아닌 자체 PBKDF2 — 로그인 가능 더미/테스트 계정 seed 시 이 포맷으로 직접 생성해야 함"
|
||||
body_draft: |
|
||||
# bibimbap 인증: PBKDF2 자체 해시 (BCrypt 아님)
|
||||
|
||||
Spring Security 미사용. 로그인은 UserController 자체 구현 password 검증.
|
||||
password_hash 포맷: `pbkdf2_sha256$210000$<base64(salt)>$<base64(hash)>` ($ 4필드)
|
||||
- 알고리즘 PBKDF2WithHmacSHA256, iterations=210000, keyLength=256bit, salt=16byte SecureRandom
|
||||
- 검증 경로: UserController.verifyPassword (parts[0]=="pbkdf2_sha256" && len==4 확인 후 동일 iter/salt 재계산, MessageDigest.isEqual 상수시간 비교)
|
||||
|
||||
## Why
|
||||
외부 BCrypt 생성기로 만든 해시는 무효 — 형식 자체가 다름. 향후 로그인 가능 더미/테스트/관리자 계정을
|
||||
seed 할 때마다 이 함정에 반복적으로 부딪힌다. 코드로 유도는 가능하나 "BCrypt 일 것"이라는 기본 가정이
|
||||
강해 매번 재확인 비용이 든다.
|
||||
|
||||
## How to apply
|
||||
- 로그인 가능 계정 seed 시 위 PBKDF2 스펙으로 해시 문자열을 직접 생성.
|
||||
- 로그인 동작 보장은 실제 verifyPassword 코드 경로(JDK 컨테이너 교차 실행 등)로 positive/negative 둘 다 확인.
|
||||
- 로그인 필수 조건 동봉: users.status='ACTIVE', user_auth_identities.provider='email',
|
||||
provider_user_id=정규화(trim+lowercase) 이메일, 양쪽 is_delete=false.
|
||||
rationale_for_saving: "코드로만 드러나는 비자명 사실(BCrypt 아님)이고, 테스트/더미 계정 seed 마다 재발하는 함정. schema/git log 로는 '자체 PBKDF2 라서 외부 생성기 무효'라는 함의가 드러나지 않음."
|
||||
signal_source: observation
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/CLAUDE.md
|
||||
- name: bibimbap-seed-blueprint-fk-and-unique
|
||||
type: reference
|
||||
description: "dev 스키마 더미 seed 청사진 — FK 순서 + game_reviews active-unique 제약(게임당 user 1리뷰)"
|
||||
body_draft: |
|
||||
# bibimbap dev 더미 seed 청사진
|
||||
|
||||
현실적 경로 = dev 스키마에 psql 직접 INSERT (UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용).
|
||||
DB 직삽은 CSRF·세션·중복선검사를 우회하되 FK·CHECK·유니크idx 는 그대로 적용.
|
||||
|
||||
INSERT 순서(FK 의존):
|
||||
1. users (status='ACTIVE' 필수, role='USER', is_delete=false, display_name 권장)
|
||||
2. user_auth_identities (PBKDF2 password_hash — 별도 memory 참조; provider='email')
|
||||
3. games (user_id FK, name; is_visible/is_delete default 로 목록노출 충족; webgl/thumbnail NULL 가능)
|
||||
4. game_reviews (game_id, user_id, rating 1~5, body)
|
||||
5. game_comments (game_id, user_id, nickname[직접 채움 — 조회가 users JOIN 안 함], content)
|
||||
|
||||
## 핵심 제약
|
||||
- game_reviews 부분 유니크 idx `(game_id, user_id) WHERE is_delete IS NOT TRUE`
|
||||
→ 한 게임에 더미 리뷰 N개 = 더미 유저 N명 필요 (한 user 는 게임당 활성 1리뷰).
|
||||
- game_comments 는 유니크 없음 → 자유 다수. 작성자명 보이려면 nickname 직접 채움(비정규화 저장).
|
||||
- 게임 목록+상세 노출 = games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false.
|
||||
|
||||
## Why
|
||||
리뷰 N개를 같은 유저로 넣으려다 유니크 위반으로 막히는 게 첫 시도의 흔한 실패. schema.sql 이
|
||||
'비권위 복원본'이라 제약을 코드에서 직접 읽기 전엔 드러나지 않음.
|
||||
|
||||
## How to apply
|
||||
- seed SQL 확정 직전 `\d dev.<table>` 로 제약/타입/default 운영 대조 권장(schema.sql 비권위).
|
||||
- 리뷰 다양화가 목표면 더미 유저를 리뷰 수만큼 선행 INSERT.
|
||||
rationale_for_saving: "seed 설계 시 반복되는 유니크-위반 함정 + FK 순서. schema.sql 비권위라 코드 교차확인 없이는 유도 불가하고, 더미/픽스처 작업마다 재사용되는 청사진."
|
||||
signal_source: observation
|
||||
docs_sync_target: null
|
||||
- name: java-crypto-verify-via-jdk-container-when-no-host-jdk
|
||||
type: feedback
|
||||
description: "호스트에 JDK 없을 때 Java 암호/검증 로직을 eclipse-temurin:21-jdk 컨테이너 single-file 실행으로 교차검증"
|
||||
body_draft: |
|
||||
# 호스트 JDK 부재 시 Java 로직 교차검증 패턴
|
||||
|
||||
호스트에 JDK 가 없고 앱도 미가동인 환경에서, password_hash 같은 Java 암호/검증 로직의
|
||||
정합성을 추정으로 닫지 않고 `eclipse-temurin:21-jdk` 컨테이너에 single-file/javac 로
|
||||
실제 코드 경로(예: verifyPassword)를 옮겨 실행해 positive+negative 케이스를 모두 확인.
|
||||
|
||||
## Why
|
||||
seed 한 해시가 "로그인 된다"는 self-report 는 증거가 아니다. 실제 검증 함수와 동일한
|
||||
알고리즘/iter/salt 로 재계산해 true/false 가 나와야 AC 가 닫힌다. 호스트 toolchain 부재는
|
||||
검증 생략의 이유가 되지 않고, 컨테이너로 우회 가능하다.
|
||||
|
||||
## How to apply
|
||||
- 검증 대상 메서드의 알고리즘 파라미터(iter/keyLength/salt/encoding)를 코드에서 그대로 추출.
|
||||
- 동일 로직을 단일 .java 로 재현해 temurin 컨테이너에서 실행, 정답=true / 오답=false 둘 다 확인.
|
||||
- 런타임 UI 스모크처럼 컨테이너로도 불가한 부분만 needs_user_verification 으로 이월.
|
||||
rationale_for_saving: "환경 제약(호스트 JDK 없음) + Java 검증 로직이라는 비자명 조합. self-report 대신 실행 증거로 AC 를 닫는 재현성 있는 검증 워크플로우 — 단발 우발 아님."
|
||||
signal_source: positive
|
||||
docs_sync_target: /Users/wemadeplay/workspace/stz/bibimbap/docs/development/verification-strategies.md
|
||||
protocol_feedback: []
|
||||
applied_changes: []
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
phase: research
|
||||
agent: research-advisor
|
||||
agent_version: 2
|
||||
generated_at: 2026-06-18T03:18:28Z
|
||||
concerns:
|
||||
- "low source confidence — db/schema.sql 은 헤더 주석상 '비권위 복원본'(users/user_auth_identities/game_comments). 컬럼명·매퍼 일치는 코드로 교차확인됐으나 타입·길이·기본값·제약은 추론값. 운영 DB 제약과 다를 수 있으므로 seed SQL 확정 전 실 DB(\\d 조회)로 컬럼·제약 대조 필요."
|
||||
- "BCrypt 가 아닌 PBKDF2 자체 해시이므로 seed용 password_hash 문자열은 외부 BCrypt 생성기로 만들 수 없음. iterations=210000/HmacSHA256/16byte-salt/256bit 스펙에 맞춰 직접 생성해야 함(생성 스크립트 자체는 design/구현 몫)."
|
||||
concerns_checked: true
|
||||
source_confidence: high
|
||||
workers_spawned: 3
|
||||
---
|
||||
|
||||
# 조사 결과
|
||||
|
||||
## 주제
|
||||
|
||||
bibimbap(Java 21 / Spring Boot / JSP / MyBatis, WAR) 에 테스트용 더미 데이터를 seed 하기 위한 사전 조사.
|
||||
"로그인·리뷰 작성이 실제로 동작하는" 형태로 (1) 게임 (2) 로그인 가능 계정 (3) 더미 리뷰+댓글 을 만들기 위한
|
||||
정확한 컬럼값·메커니즘 도출. 코드 수정 없음.
|
||||
|
||||
> 신뢰도 표기: `확인됨` = 코드/실행으로 1차 확인. `추정` = 통용·유추. `미확인` = 검증 불가.
|
||||
> 본 조사의 사실 항목은 전부 프로젝트 코드 직접 확인 기반 → `source_confidence: high`.
|
||||
> 다만 schema.sql 자체가 "비권위 복원본"이라는 메타 한계는 concerns 에 보존(컬럼명은 매퍼로 교차확인됨, 제약·타입은 추론).
|
||||
|
||||
---
|
||||
|
||||
## 포인트별 발견
|
||||
|
||||
### 포인트 A: 로그인/인증 메커니즘 (실제 로그인 가능한 계정 seed)
|
||||
|
||||
- 경로: `src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java`,
|
||||
`data/UserAuthIdentityData.java`, `mapper/UserAuthIdentitiesMapper.java`, `mapper/UsersMapper.java`, `db/schema.sql`
|
||||
|
||||
요약:
|
||||
- **Spring Security 미사용.** `PasswordEncoder|SecurityFilterChain|@EnableWebSecurity|spring-boot-starter-security`
|
||||
src/ + pom.xml 0 hit. 인증은 UserController 자체 구현 password 로그인. **OAuth 없음**(google/kakao/naver/oauth 0 hit). [확인됨]
|
||||
- 로그인 엔드포인트: `POST /login`, form 파라미터 `email`,`password`,`remember`(선택) — UserController.java:122, 125-128. [확인됨]
|
||||
진입부에서 `CsrfTokens.isValid(request)` 실패 시 403 — UserController.java:131-133. [확인됨]
|
||||
- **provider 컬럼 값은 `"email"` 단 하나.** `PROVIDER_EMAIL="email"` — UserController.java:42.
|
||||
identity 생성 시 set — UserController.java:314. 조회 필터도 동일 값 — UserController.java:140-143. [확인됨]
|
||||
`provider_user_id` 에는 **정규화 이메일**(trim+lowercase) 저장 — UserController.java:104-106, 527-533. [확인됨]
|
||||
- **password_hash 알고리즘 = PBKDF2 (BCrypt 아님).** PasswordEncoder 빈 없음. 형식:
|
||||
`pbkdf2_sha256$<iterations>$<base64(salt)>$<base64(hash)>` (`$` 4필드) — UserController.java:554-556. [확인됨]
|
||||
- iterations=210000 (UserController.java:46), keyLength=256bit (:47), salt=16byte SecureRandom (:48,551-552),
|
||||
알고리즘 `PBKDF2WithHmacSHA256` via JCE SecretKeyFactory/PBEKeySpec (:576-584). [확인됨]
|
||||
- 검증: `verifyPassword()` 가 `$` split → parts[0]=="pbkdf2_sha256" && length==4 확인 후 동일 iterations/salt 재계산,
|
||||
`MessageDigest.isEqual` 상수시간 비교 — UserController.java:144, 559-574. [확인됨]
|
||||
- PBKDF2 표준 동작(iterations·salt 가 해시 문자열에 동봉되어 검증 시 재현 가능) — [추정/일반 통용 지식]
|
||||
- **로그인 성공 시 세션 저장** (saveLoginSession — UserController.java:162, 502-525). 세션 직전 `changeSessionId()` 세션고정방어 — :159-160. [확인됨]
|
||||
개별 attribute: `id`(Long), **`userId`(Long — 인증가드 sessionUserId() 가 읽는 키, :337)**, `displayName`, `email`(canonicalEmail),
|
||||
`avatarUrl`, `role`, `status`, `authProvider`("email"), `authIdentityId`, `lastLoginAt` — UserController.java:503-512.
|
||||
추가로 `account` 키에 위 값들의 LinkedHashMap 통째 저장 — :514-524. (UserData 객체 자체가 아니라 개별 스칼라+Map) [확인됨]
|
||||
- **로그인 검증 컬럼 흐름** [확인됨]:
|
||||
1. user_auth_identities `WHERE provider='email' AND provider_user_id=<정규화이메일> AND is_delete IS NOT TRUE` — UserAuthIdentitiesMapper.java:47-49
|
||||
2. password_hash != null && PBKDF2 통과 — UserController.java:144
|
||||
3. users `WHERE id=<identity.user_id> AND is_delete IS NOT TRUE` — UsersMapper.java:25-26
|
||||
4. **user.status == "ACTIVE"** 필수 (STATUS_ACTIVE=:44) — UserController.java:148-149
|
||||
|
||||
- **seed 시 필수 채움 컬럼** (schema.sql 비권위 — 컬럼명만 신뢰):
|
||||
- users: `role`(default 'USER'), **`status`='ACTIVE'**(ACTIVE 아니면 로그인 거부), `is_delete`=false. display_name/canonical_email 권장. — schema.sql:30-42 [확인됨, 제약값은 추정]
|
||||
- user_auth_identities: `user_id`(FK), `provider`='email', `provider_user_id`=정규화이메일(소문자), **`password_hash`=PBKDF2형식**(null이면 즉시거부), `is_delete`=false. — schema.sql:49-63 [확인됨, 제약값은 추정]
|
||||
- 주의: active-unique idx `ux_user_auth_identities_provider_user_id_active` = `(provider, provider_user_id) WHERE is_delete IS NOT TRUE` — schema.sql:66-68. 같은 이메일 활성 identity 중복 INSERT 실패. [확인됨, idx는 비권위 schema.sql]
|
||||
- 신뢰도: 확인됨 (코드 직접 확인). schema.sql 유래 제약값만 추정.
|
||||
|
||||
### 포인트 B: 리뷰/댓글 작성 시 유저 귀속 + INSERT 컬럼 + CSRF
|
||||
|
||||
- 경로: `controller/api/GameReviewController.java`, `data/GameReviewData.java`, `mapper/GameReviewsMapper.java`,
|
||||
`controller/api/GameCommentController.java`, `data/GameCommentData.java`, `mapper/GameCommentsMapper.java`,
|
||||
`security/CsrfTokens.java`, `db/schema.sql`, `docs/game-reviews-ddl.sql`
|
||||
|
||||
요약:
|
||||
- **리뷰 API**: GET `/game/{id}/reviews`(목록, :41-42), GET `/game/{id}/reviews/{reviewId}`(:58-62),
|
||||
POST `/game/{id}/reviews`(작성, :74), PUT(수정, :124), DELETE(소프트삭제, :170) — GameReviewController.java. [확인됨]
|
||||
작성 바디(@RequestParam, form): `rating`(String→1~5 파싱, 범위밖 400), `body`(최대 1000자) — :78-101, BODY_MAX=:30. [확인됨]
|
||||
- **user_id 출처 = 세션 attribute `"userId"`** (요청 바디 아님). `sessionUserId(session)` → `session.getAttribute("userId")` — GameReviewController.java:86, 241-257(:245). 없으면 401. `review.setUserId(userId)` — :109. [확인됨]
|
||||
- **game_reviews INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, rating, body)` `#{}` 바인딩 — GameReviewsMapper.java:72-86. [확인됨]
|
||||
- game_id ← PathVariable(:108), user_id ← 세션(:109), rating ← 1~5 정수(:110; DB CHECK BETWEEN 1 AND 5 — schema.sql:129, docs/game-reviews-ddl.sql:49-53), body ← trimToEmpty(빈입력시 ""; DB text nullable) — :98,275-278. [확인됨]
|
||||
- 미포함(전부 DB DEFAULT): id(seq), created_at/updated_at(now()), is_delete(false), deleted_at(null) — schema.sql:124-127. [확인됨, 비권위 schema]
|
||||
- 참고: updated_at > created_at 이면 조회 시 edited=true 표시 — GameReviewsMapper.java:24, GameReviewController.java:210. [확인됨]
|
||||
- **ux_game_reviews_game_user_active** = 부분 유니크 인덱스 `(game_id, user_id) WHERE is_delete IS NOT TRUE` — schema.sql:132-133, docs/game-reviews-ddl.sql:58-61. [확인됨]
|
||||
의미: 활성 리뷰는 (game,user) 조합당 1개. soft-delete 행은 제외 → 재작성 허용. 코드도 INSERT 전 중복 선검사 후 409 — GameReviewController.java:103-105, GameReviewsMapper.java:56-70. [확인됨]
|
||||
**seed 주의**: 한 게임에 더미 리뷰 여러 개 원하면 각 행 user_id 를 다르게. 한 user 는 게임당 활성 1개. [확인됨]
|
||||
- **댓글 API**: GET `/game/{id}/comments`(:39), POST(작성, :56), PUT(:102), DELETE(소프트삭제, :143). 작성 바디 `content`만(최대 200자, CONTENT_MAX=:28) — GameCommentController.java:60,75-78. [확인됨]
|
||||
- user_id ← 세션 `"userId"` (:67,198). [확인됨]
|
||||
- **nickname 비정규화 저장 확정**: 작성 시점 세션 `"displayName"` 을 nickname 컬럼에 복사 — `comment.setNickname(authorName)` :84, sessionDisplayName=session.getAttribute("displayName") :212-218(:216). 조회 시 users JOIN 없이 nickname 그대로 노출(`nickname AS authorName`) — GameCommentsMapper.java:36, GameCommentController.java:179. [확인됨]
|
||||
- **game_comments INSERT 컬럼 = 정확히 4개**: `(game_id, user_id, nickname, content)` — GameCommentsMapper.java:46-60. user_id nullable FK(schema.sql:112), nickname varchar(100) nullable(:102). [확인됨, 비권위 schema]
|
||||
- 유니크 제약 없음 → 같은 (game,user) 댓글 여러 개 가능. 화면 작성자명 보이려면 nickname 직접 채워야(조회가 users JOIN 안 함). 목록 정렬 created_at ASC, id ASC — GameCommentsMapper.java:42. [확인됨]
|
||||
- **CSRF**: Spring Security 미사용 → 표준 CSRF 필터 없음. 대신 자체 `CsrfTokens` 유틸을 컨트롤러 mutation 진입부에서 수동검증. [확인됨]
|
||||
- 리뷰 POST/PUT/DELETE: GameReviewController.java:83-85,134-136,178-180. 댓글: GameCommentController.java:64-66,111-113,151-153. GET 은 검증 없음. [확인됨]
|
||||
- 토큰: 세션 key `"csrfToken"`, 헤더 `X-CSRF-Token`(우선) 또는 폼 파라미터 `_csrf`(폴백), 32byte SecureRandom Base64url — CsrfTokens.java:12-13,20-33,35-49. [확인됨]
|
||||
- **영향**: HTTP API 경유 수동 작성은 로그인세션+csrfToken+헤더/파라미터 필요. **DB 직접 INSERT seed 는 CSRF·세션·중복선검사 모두 우회**(단 DB 유니크idx·CHECK·FK 는 그대로 적용). [확인됨]
|
||||
- 신뢰도: 확인됨. schema.sql/ddl 유래 제약만 추정.
|
||||
|
||||
### 포인트 C: seed 적용 경로 + 게임 표시 조건
|
||||
|
||||
- 경로: `.env`, `docker-compose.yml`, `src/main/resources/dev/db.properties`, `GameController.java`, `GamesMapper.java`,
|
||||
`data/GameData.java`, `db/schema.sql`, `WEB-INF/views/game-detail.jsp`
|
||||
|
||||
요약:
|
||||
- **DB 접속 정보 확정** (.env + docker-compose.yml + dev/db.properties 일관) [확인됨, 실행 확인]:
|
||||
- DB명 `bibimbap`, 유저 `bibimbap`, 패스워드 `change_me_local_dev`, 호스트포트 5433 → 컨테이너 5432.
|
||||
- .env: POSTGRES_DB/USER=bibimbap, POSTGRES_PASSWORD=change_me_local_dev, APP_SCHEMA=dev, DB_PORT=5433
|
||||
- docker-compose.yml:18 `"${DB_PORT:-5432}:5432"`, :40 `jdbc:...:5432/${POSTGRES_DB}?currentSchema=${APP_SCHEMA:-dev}`
|
||||
- dev/db.properties:2-4 `jdbc:postgresql://localhost:5433/bibimbap?currentSchema=dev`
|
||||
- 컨테이너 `bibimbap-db postgres:16 Up (healthy) 0.0.0.0:5433->5432/tcp` (docker compose ps 실행 확인)
|
||||
- **앱 스키마 = `dev`**. psql 접속: `PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap` 후 `SET search_path TO dev;` (또는 `dev.games` 한정).
|
||||
- **현재 dev.games / dev.users 모두 0행** (read-only 조회 확인). live 스키마는 테이블 없음 → 앱 데이터는 전적으로 dev. [확인됨]
|
||||
- **게임 표시 조건** (목록과 상세가 다름; games-users INNER JOIN `JOIN users u ON u.id=g.user_id` → 유효 미삭제 user 필수) — GamesMapper.java:32-33. [확인됨]
|
||||
- 상세(getGame, GamesMapper.java:34-36): `g.id=#{id} AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. **is_visible 조건 없음** → 비공개 게임도 id 직접 접근 시 상세 뜸. [확인됨]
|
||||
- 목록(getVisibleGames, GamesMapper.java:57-59): `g.is_visible IS NOT FALSE AND g.is_delete IS NOT TRUE AND u.is_delete IS NOT TRUE`. [확인됨]
|
||||
- **목록+상세 모두 보이려면**: games.is_visible=true, games.is_delete=false, 연결 users.is_delete=false. [확인됨]
|
||||
- 컨트롤러: gameDetail 이 getGame(id)!=null 이면 DB 게임으로 game-detail 렌더 — GameController.java:113-121. [확인됨]
|
||||
- **games INSERT 필수(NOT NULL, no-default) 컬럼 = `user_id`(FK→users), `name` 둘뿐** — schema.sql:75-87. [확인됨, 비권위 schema]
|
||||
- default 보유(생략가능): like_count(0), is_visible(true), sort_order(0), created_at/updated_at(now()), is_delete(false). nullable: creator_note, git_url, webgl_path, thumbnail_url. id=seq.
|
||||
- 최소 INSERT `(user_id, name)` 만으로 가능 + 목록노출 조건 자동충족. 단 user_id FK 로 **users 행 선행 필수**.
|
||||
- **asset 컬럼 NULL 이어도 상세/리뷰 동작 — 깨지지 않음** [확인됨]:
|
||||
- game-detail.jsp:12-21 webglFrameSrc null/blank/"null" 시 빈 문자열 폴백 → iframe src="" 빈화면(예외 없음). 컨트롤러가 trimToEmpty 정규화 후 모델 주입(GameController.java:273,281-283)이라 NPE 없음.
|
||||
- thumbnail_url 은 상세 JSP 미사용. 리뷰/댓글은 별도 비동기 API(`/game/{id}/reviews`,`/comments`)로 로드 → game asset 컬럼과 무관.
|
||||
- 결론: webgl_path/thumbnail_url 을 NULL 로 둬도 상세 + 리뷰/댓글 테스트 가능.
|
||||
- 신뢰도: 확인됨 (코드 + 실행). schema.sql 유래 제약만 추정.
|
||||
|
||||
---
|
||||
|
||||
## 종합 판단 (더미 seed 실행 청사진)
|
||||
|
||||
세 포인트가 한 흐름으로 맞물린다. **현실적 seed 경로 = dev 스키마에 psql 직접 INSERT** (앱 UI 경유는 CSRF+세션+WebGL 업로드 강제로 고비용; DB 직삽은 그 전부 우회하되 FK·CHECK·유니크idx 만 적용).
|
||||
|
||||
INSERT 순서(FK 의존성):
|
||||
1. **users** 1행: status='ACTIVE'(필수 — 아니면 로그인 거부), role='USER', is_delete=false, display_name 권장(목록 creator + 댓글 nickname 소스).
|
||||
2. **user_auth_identities** 1행: user_id=위 user, provider='email', provider_user_id=<정규화 소문자 이메일>, password_hash=**PBKDF2 형식** `pbkdf2_sha256$210000$<b64salt>$<b64hash>`, is_delete=false. (활성 유니크 idx 주의)
|
||||
3. **games** 1행: user_id=위 user, name=값. is_visible/is_delete 는 default 로 목록노출 충족. webgl_path/thumbnail_url NULL 가능.
|
||||
4. **game_reviews** N행: (game_id, user_id, rating 1~5, body). **한 게임당 user 1활성리뷰** → 여러 리뷰면 user 여러 개 seed.
|
||||
5. **game_comments** N행: (game_id, user_id, nickname[직접 채움], content). 유니크 없음 → 자유 다수.
|
||||
|
||||
**로그인 가능 핵심(질문의 최우선 관심사)**: password_hash 는 BCrypt 가 아니라 PBKDF2WithHmacSHA256.
|
||||
평문→해시는 외부 BCrypt 도구로 만들 수 없고, iterations=210000 / 256bit / 16byte salt / base64 스펙으로
|
||||
`pbkdf2_sha256$210000$<base64salt>$<base64hash>` 문자열을 생성해 넣어야 검증 통과한다.
|
||||
(해시 생성 스크립트 작성은 design/구현 단계 몫 — research 범위 밖.)
|
||||
|
||||
권위 격상 전 검증 필요 항목: db/schema.sql 이 비권위 복원본이므로, 위 컬럼 *제약/타입/default* 는 seed SQL 확정 직전
|
||||
실 DB `\d dev.users / \d dev.user_auth_identities / \d dev.games / \d dev.game_reviews / \d dev.game_comments` 로 대조 권장.
|
||||
(컬럼명·매퍼 일치·인덱스 존재는 코드/실행으로 확인됨 — 불일치 위험은 타입/제약값에 한정.)
|
||||
|
||||
## 미해결
|
||||
|
||||
- PBKDF2 seed 해시를 실제로 생성하는 도구/스크립트는 미작성(설계·구현 영역). 본 조사는 "어떤 형식이어야 검증 통과하는가"까지만.
|
||||
- db/schema.sql 비권위로 인한 컬럼 *제약/타입/default* 의 운영 DB 대조는 미수행(읽기조회로 행수·테이블존재만 확인). seed SQL 확정 전 `\d` 대조 필요.
|
||||
- game_likes 서버측 like_count 증감 경로(좋아요는 localStorage 클라이언트 토글로 보임) — 더미 리뷰/계정/게임 범위 밖이라 미조사.
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# bibimbap 인증: 자체 PBKDF2 해시 (BCrypt 아님)
|
||||
|
||||
Spring Security 미사용. 로그인은 `UserController` 자체 구현 password 검증.
|
||||
|
||||
password_hash 포맷: `pbkdf2_sha256$<iter>$<base64(salt)>$<base64(hash)>` ($ 4필드)
|
||||
- 알고리즘 `PBKDF2WithHmacSHA256`, iterations=210000, keyLength=256bit(=32byte), salt=16byte SecureRandom
|
||||
- base64 = 표준(padding 포함). 검증: `UserController.verifyPassword`
|
||||
(`src/main/java/com/pandoli365/bibimbap/controller/api/UserController.java:46-48, 550-584`).
|
||||
parts[0]=="pbkdf2_sha256" && len==4 확인 → 동일 iter/salt 재계산 → `MessageDigest.isEqual` 상수시간 비교.
|
||||
|
||||
## 함정 (Why save)
|
||||
외부 BCrypt 생성기로 만든 해시는 **무효** — 형식 자체가 다름. 코드 안 보면 "BCrypt 일 것"이라는
|
||||
기본 가정이 강해, 로그인 가능 더미/테스트/관리자 계정 seed 마다 반복적으로 부딪힌다.
|
||||
|
||||
## 로그인 가능 계정 seed 시 필수 조건
|
||||
- `users.status = 'ACTIVE'` (UserController.java:148-149, 미충족 시 로그인 거부)
|
||||
- `user_auth_identities.provider = 'email'` (단일 provider, UserController.java:42)
|
||||
- `provider_user_id` = 정규화(trim + lowercase) 이메일 (UserController.java:527-533)
|
||||
- 양쪽 `is_delete = false`
|
||||
- 세션 귀속 키: 로그인 성공 시 session attribute `userId`(Long) 저장 → 리뷰/댓글 작성 가드가 이 키를 읽음.
|
||||
|
||||
## 평문→해시 생성 (Python, Java 와 byte-identical)
|
||||
```python
|
||||
import hashlib, base64, os
|
||||
salt = os.urandom(16)
|
||||
dk = hashlib.pbkdf2_hmac('sha256', pw.encode(), salt, 210000, dklen=32)
|
||||
h = f"pbkdf2_sha256$210000${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}"
|
||||
```
|
||||
로그인 동작 보장은 실제 `verifyPassword` 코드 경로로 positive/negative 둘 다 확인할 것
|
||||
(호스트 JDK 없으면 `mem:java-crypto-verify-via-jdk-container-when-no-host-jdk` 참조).
|
||||
|
||||
근거 세션: .atp/work-session/20260618-121419 (더미 seed). 관련 산출물 `db/seed-dev.sql`.
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
# bibimbap dev 더미 seed 청사진
|
||||
|
||||
현실적 경로 = **dev 스키마에 psql 직접 INSERT**. UI 경유는 CSRF + 세션 + (게임) WebGL 업로드를
|
||||
강제해 고비용. DB 직삽은 CSRF/세션/중복선검사를 우회하되 FK·CHECK·UNIQUE idx 는 그대로 적용됨.
|
||||
|
||||
기성 산출물: `db/seed-dev.sql` (멱등, NOT EXISTS 가드) + `db/seed-dev-teardown.sql` (정리).
|
||||
적용: `docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev.sql`
|
||||
DB: 가동 컨테이너 `bibimbap-db` (postgres:16, 호스트 5433), db/user `bibimbap`, schema `dev`.
|
||||
|
||||
## INSERT 순서 (FK 의존)
|
||||
1. `users` (status='ACTIVE', role='USER', is_delete=false)
|
||||
2. `user_auth_identities` (PBKDF2 password_hash — `mem:bibimbap-auth-pbkdf2-not-bcrypt`; provider='email')
|
||||
3. `games` (user_id FK, name NOT NULL, is_visible=true, is_delete=false → 목록/상세 표시; sort_order=MAX+1)
|
||||
4. `game_reviews` (game_id, user_id, rating 1~5 CHECK, body)
|
||||
5. `game_comments` (game_id, user_id nullable, nickname, content)
|
||||
|
||||
## 핵심 제약 (코드 교차확인 필요 — schema.sql 은 비권위 복원본)
|
||||
- **`game_reviews` 부분 UNIQUE `(game_id, user_id) WHERE is_delete IS NOT TRUE`**
|
||||
→ 한 게임에 더미 리뷰 N개 = **더미 유저 N명** 필요. 한 유저로 N리뷰 불가.
|
||||
- `game_reviews`/`games` 조회는 `users` JOIN → 작성자명은 `users.display_name` 에서 옴.
|
||||
- `game_comments` 조회는 users JOIN 안 함 → **작성자명은 `nickname` 컬럼을 직접 채워야** 표시됨
|
||||
(`GameCommentsMapper.listGameComments`).
|
||||
- 게임 `webgl_path`/`thumbnail_url` NULL 안전 — `game-detail.jsp:13-21` 가 빈 src 로 처리.
|
||||
- "리뷰 작성 테스트" 하려면 로그인 계정은 해당 게임에 리뷰 0건이어야 함
|
||||
(`getActiveReviewByGameAndUser` 중복검사, GameReviewController.java:103-105).
|
||||
|
||||
근거 세션: .atp/work-session/20260618-121419.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# 호스트 JDK 부재 시 Java 암호 로직 교차검증 패턴
|
||||
|
||||
bibimbap 호스트엔 JDK 없음(빌드/실행은 Docker 경로). 그래서 생성한 password_hash 등이
|
||||
**실제 Java 코드 경로로 검증되는지**를 추정으로 닫지 말고 컨테이너로 실행 증거를 만든다.
|
||||
|
||||
## 패턴
|
||||
1. 검증 대상 로직을 단일 .java 로 복제(예: `UserController.verifyPassword` 그대로).
|
||||
2. JRE-only 이미지(`bibimbap-app:latest`)는 `java File.java` 소스모드 불가
|
||||
(`jdk.compiler` 모듈 없음 → `InternalError: Module jdk.compiler not in boot Layer`).
|
||||
3. JDK 이미지로 컴파일+실행:
|
||||
```
|
||||
docker run --rm -v /tmp/HashVerify.java:/tmp/HashVerify.java:ro \
|
||||
--entrypoint sh eclipse-temurin:21-jdk \
|
||||
-c 'javac -d /out /tmp/HashVerify.java && java -cp /out HashVerify'
|
||||
```
|
||||
4. **positive + negative 둘 다** 확인(올바른 비번=true, 틀린 비번=false). 한쪽만 true 는 불충분.
|
||||
|
||||
## 왜
|
||||
- self-report("스펙상 맞을 것") 대신 실행 증거로 AC 를 닫음.
|
||||
- Python `hashlib.pbkdf2_hmac('sha256',...)` 와 Java `PBKDF2WithHmacSHA256` 는 byte-identical 이지만,
|
||||
"동일하다"는 가정 자체를 실제 Java 경로 실행으로 검증.
|
||||
- 호스트 `timeout` 명령은 macOS 기본 미존재(gtimeout). `/usr/bin/java` 는 JRE 없는 스텁일 수 있음.
|
||||
|
||||
일반화: 호스트에 런타임 없을 때 "추정으로 닫기" 대신 throwaway 컨테이너로 실행 증거 확보.
|
||||
근거 세션: .atp/work-session/20260618-121419.
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Task Completion — bibimbap
|
||||
|
||||
코딩 작업 완료 시:
|
||||
1. `./mvnw test` 통과 (JUnit). `BibimbapApplicationTests` 는 컨텍스트 로드 — 유효 datasource(또는 스킵 조건) 필요.
|
||||
1. `./mvnw test` 통과 (JUnit). `BibimbapApplicationTests` 는 컨텍스트 로드 — 유효 datasource(또는 스킵 조건) 필요. **MyBatis/DataSource autoconfigure 가 exclude 돼 있어 @Mapper 빈이 자동생성되지 않으므로, 컨트롤러가 주입하는 매퍼마다 `@MockBean` 을 수동 등록해야 한다.** 신규 컨트롤러를 추가하거나 컨트롤러의 매퍼 의존을 늘리면 `BibimbapApplicationTests` 에 해당 매퍼 `@MockBean` 을 추가하지 않는 한 `contextLoads` 가 `NoSuchBeanDefinitionException` 으로 실패한다. `test-compile` 만으로는 못 잡으므로 반드시 full `./mvnw test` 로 확인 (W3-2 세션 20260618 회귀 실증).
|
||||
2. `./mvnw clean package` 로 WAR 빌드 확인 — JSP/컴파일 오류는 단위테스트가 못 잡으므로 패키지까지 돌려 확인.
|
||||
3. 린터/포매터 없음 (spotless/checkstyle 미설정) — 실행할 것 없음.
|
||||
4. DB 스키마 변경 시: `src/test/db/dev-to-live-update.sql` + `DbUpdateQueryGeneratorTest` 패턴 참조 (dev→live 마이그레이션 쿼리 생성).
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
-- =============================================================================
|
||||
-- bibimbap dev seed teardown — seed-dev.sql 로 만든 더미 데이터만 정리
|
||||
-- =============================================================================
|
||||
-- seed-dev.sql 이 생성한 더미(@bibimbap.local 유저 + '테스트 게임 (더미)' + 그
|
||||
-- 게임의 리뷰/댓글)만 hard delete 한다. 운영성 데이터는 건드리지 않는다.
|
||||
--
|
||||
-- 적용:
|
||||
-- docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev-teardown.sql
|
||||
-- PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap -f db/seed-dev-teardown.sql
|
||||
--
|
||||
-- FK 의존 역순 삭제: reviews/comments/likes → games → auth_identities → users.
|
||||
-- =============================================================================
|
||||
|
||||
SET search_path TO dev;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_game_id bigint;
|
||||
BEGIN
|
||||
SELECT id INTO v_game_id FROM games WHERE name = '테스트 게임 (더미)' LIMIT 1;
|
||||
|
||||
IF v_game_id IS NOT NULL THEN
|
||||
DELETE FROM game_reviews WHERE game_id = v_game_id;
|
||||
DELETE FROM game_comments WHERE game_id = v_game_id;
|
||||
DELETE FROM game_likes WHERE game_id = v_game_id;
|
||||
DELETE FROM games WHERE id = v_game_id;
|
||||
END IF;
|
||||
|
||||
DELETE FROM user_auth_identities
|
||||
WHERE user_id IN (SELECT id FROM users WHERE canonical_email LIKE '%@bibimbap.local');
|
||||
|
||||
DELETE FROM users WHERE canonical_email LIKE '%@bibimbap.local';
|
||||
|
||||
RAISE NOTICE 'seed-dev teardown 완료 (game_id=%)', v_game_id;
|
||||
END $$;
|
||||
|
||||
SELECT
|
||||
(SELECT count(*) FROM users WHERE canonical_email LIKE '%@bibimbap.local') AS remaining_dummy_users,
|
||||
(SELECT count(*) FROM games WHERE name = '테스트 게임 (더미)') AS remaining_dummy_games;
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
-- =============================================================================
|
||||
-- bibimbap dev seed — 리뷰/댓글(W3-2) 기능 테스트용 더미 상황
|
||||
-- =============================================================================
|
||||
-- 목적: 새로 추가된 게임 리뷰/댓글 기능을 실제로 테스트할 수 있는 더미 데이터.
|
||||
-- 1) 로그인 가능한 테스트 계정 1개 (직접 로그인 → 리뷰/댓글 작성 테스트용)
|
||||
-- 2) 표시되는 더미 게임 1개 (테스트 계정 소유)
|
||||
-- 3) 이미 등록된 더미 리뷰 5개 (각자 다른 더미 유저 — game당 user 1리뷰 제약 때문)
|
||||
-- + 보너스: 더미 댓글 3개
|
||||
--
|
||||
-- 멱등(idempotent): 같은 이메일/게임명 기준 NOT EXISTS 가드 → 재실행해도 중복 미생성.
|
||||
--
|
||||
-- 적용 (가동중인 컨테이너):
|
||||
-- docker exec -i bibimbap-db psql -U bibimbap -d bibimbap -v ON_ERROR_STOP=1 < db/seed-dev.sql
|
||||
-- 적용 (호스트 psql, 포트 5433):
|
||||
-- PGPASSWORD=change_me_local_dev psql -h localhost -p 5433 -U bibimbap -d bibimbap -f db/seed-dev.sql
|
||||
--
|
||||
-- 로그인 자격: tester@bibimbap.local / test1234! (provider='email')
|
||||
-- password_hash 는 UserController.hashPassword 와 동일 알고리즘으로 사전 생성:
|
||||
-- PBKDF2WithHmacSHA256 / 210000 iter / 256-bit key / 16-byte salt,
|
||||
-- 포맷 = pbkdf2_sha256$<iter>$<base64 salt>$<base64 hash>.
|
||||
--
|
||||
-- 되돌리기 (이 더미만 정리): db/seed-dev-teardown.sql 참고.
|
||||
-- =============================================================================
|
||||
|
||||
SET search_path TO dev;
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_tester_id bigint;
|
||||
v_game_id bigint;
|
||||
v_uid bigint;
|
||||
-- test1234! 의 사전 생성 해시 (위 알고리즘과 동일).
|
||||
c_pw_hash text := 'pbkdf2_sha256$210000$QJ5MV+dx3aECELuuh3ibIA==$/59CO6JWXXGXWW0hzqpnZwu89qirDozP0jlq3JzEwZw=';
|
||||
r RECORD;
|
||||
BEGIN
|
||||
-- -------------------------------------------------------------------------
|
||||
-- 1) 로그인 테스트 계정 (users + email identity)
|
||||
-- -------------------------------------------------------------------------
|
||||
SELECT id INTO v_tester_id
|
||||
FROM users
|
||||
WHERE canonical_email = 'tester@bibimbap.local' AND is_delete IS NOT TRUE
|
||||
LIMIT 1;
|
||||
|
||||
IF v_tester_id IS NULL THEN
|
||||
INSERT INTO users (display_name, canonical_email, role, status)
|
||||
VALUES ('테스터', 'tester@bibimbap.local', 'USER', 'ACTIVE')
|
||||
RETURNING id INTO v_tester_id;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM user_auth_identities
|
||||
WHERE provider = 'email' AND provider_user_id = 'tester@bibimbap.local'
|
||||
AND is_delete IS NOT TRUE
|
||||
) THEN
|
||||
INSERT INTO user_auth_identities
|
||||
(user_id, provider, provider_user_id, email, password_hash, display_name)
|
||||
VALUES
|
||||
(v_tester_id, 'email', 'tester@bibimbap.local', 'tester@bibimbap.local', c_pw_hash, '테스터');
|
||||
END IF;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- 2) 더미 게임 (테스트 계정 소유, 목록/상세에 표시되도록 visible)
|
||||
-- webgl_path / thumbnail_url 은 NULL — game-detail.jsp 가 빈 src 로 안전 처리.
|
||||
-- -------------------------------------------------------------------------
|
||||
SELECT id INTO v_game_id
|
||||
FROM games
|
||||
WHERE name = '테스트 게임 (더미)' AND is_delete IS NOT TRUE
|
||||
LIMIT 1;
|
||||
|
||||
IF v_game_id IS NULL THEN
|
||||
INSERT INTO games (user_id, name, creator_note, is_visible, sort_order)
|
||||
VALUES (
|
||||
v_tester_id,
|
||||
'테스트 게임 (더미)',
|
||||
'리뷰/댓글 기능 테스트용 더미 게임입니다. 자유롭게 리뷰와 댓글을 남겨보세요.',
|
||||
true,
|
||||
(SELECT COALESCE(MAX(sort_order), 0) + 1 FROM games WHERE is_delete IS NOT TRUE)
|
||||
)
|
||||
RETURNING id INTO v_game_id;
|
||||
END IF;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- 3) 더미 리뷰어 5명 + 각자 리뷰 1건 (game당 user 1리뷰 unique 제약 충족)
|
||||
-- created_at 을 days_ago 만큼 과거로 스태거 → 목록(최신순) 정렬이 자연스럽게.
|
||||
-- -------------------------------------------------------------------------
|
||||
FOR r IN
|
||||
SELECT * FROM (VALUES
|
||||
('김플레이', 'reviewer1@bibimbap.local', 5, '그래픽이 깔끔하고 조작감이 좋아요. 가볍게 즐기기 딱 좋습니다. 추천!', 5),
|
||||
('이도전', 'reviewer2@bibimbap.local', 4, '아이디어가 신선했습니다. 난이도 밸런스만 조금 더 다듬으면 완벽할 듯해요.', 4),
|
||||
('박캐주얼', 'reviewer3@bibimbap.local', 3, '무난하게 즐길 만한 게임. 다만 후반부가 살짝 반복적으로 느껴졌어요.', 3),
|
||||
('최열정', 'reviewer4@bibimbap.local', 5, '시간 가는 줄 모르고 플레이했네요. BGM이 특히 인상적이었습니다!', 2),
|
||||
('정라이트', 'reviewer5@bibimbap.local', 2, '초반 튜토리얼이 불친절해서 적응이 조금 어려웠어요. 보완되면 좋겠습니다.', 1)
|
||||
) AS t(nickname, email, rating, body, days_ago)
|
||||
LOOP
|
||||
SELECT id INTO v_uid
|
||||
FROM users
|
||||
WHERE canonical_email = r.email AND is_delete IS NOT TRUE
|
||||
LIMIT 1;
|
||||
|
||||
IF v_uid IS NULL THEN
|
||||
INSERT INTO users (display_name, canonical_email, role, status)
|
||||
VALUES (r.nickname, r.email, 'USER', 'ACTIVE')
|
||||
RETURNING id INTO v_uid;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM game_reviews
|
||||
WHERE game_id = v_game_id AND user_id = v_uid AND is_delete IS NOT TRUE
|
||||
) THEN
|
||||
INSERT INTO game_reviews (game_id, user_id, rating, body, created_at, updated_at)
|
||||
VALUES (
|
||||
v_game_id, v_uid, r.rating, r.body,
|
||||
now() - make_interval(days => r.days_ago),
|
||||
now() - make_interval(days => r.days_ago)
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- 보너스) 더미 댓글 3건 (리뷰어 유저 귀속, created_at ASC 정렬용 스태거)
|
||||
-- -------------------------------------------------------------------------
|
||||
FOR r IN
|
||||
SELECT * FROM (VALUES
|
||||
('reviewer1@bibimbap.local', '오 이거 재밌네요 ㅎㅎ', 3),
|
||||
('reviewer3@bibimbap.local', '다음 업데이트도 기대할게요!', 2),
|
||||
('reviewer5@bibimbap.local', '버그 제보: 가끔 화면이 멈춰요.', 1)
|
||||
) AS t(email, content, days_ago)
|
||||
LOOP
|
||||
SELECT id INTO v_uid
|
||||
FROM users
|
||||
WHERE canonical_email = r.email AND is_delete IS NOT TRUE
|
||||
LIMIT 1;
|
||||
|
||||
IF v_uid IS NOT NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM game_comments
|
||||
WHERE game_id = v_game_id AND user_id = v_uid AND content = r.content
|
||||
AND is_delete IS NOT TRUE
|
||||
) THEN
|
||||
INSERT INTO game_comments (game_id, user_id, nickname, content, created_at)
|
||||
VALUES (
|
||||
v_game_id, v_uid,
|
||||
(SELECT display_name FROM users WHERE id = v_uid),
|
||||
r.content,
|
||||
now() - make_interval(days => r.days_ago)
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
RAISE NOTICE 'seed-dev 완료: tester_id=%, game_id=%', v_tester_id, v_game_id;
|
||||
END $$;
|
||||
|
||||
-- 적용 결과 확인용 요약 (psql 실행 시 출력).
|
||||
SELECT
|
||||
(SELECT count(*) FROM users WHERE canonical_email LIKE '%@bibimbap.local' AND is_delete IS NOT TRUE) AS dummy_users,
|
||||
(SELECT count(*) FROM games WHERE name = '테스트 게임 (더미)' AND is_delete IS NOT TRUE) AS dummy_games,
|
||||
(SELECT count(*) FROM game_reviews r JOIN games g ON g.id = r.game_id
|
||||
WHERE g.name = '테스트 게임 (더미)' AND r.is_delete IS NOT TRUE) AS dummy_reviews,
|
||||
(SELECT count(*) FROM game_comments c JOIN games g ON g.id = c.game_id
|
||||
WHERE g.name = '테스트 게임 (더미)' AND c.is_delete IS NOT TRUE) AS dummy_comments;
|
||||
Loading…
Reference in New Issue