bibimbap/.atp/work-session/20260618-121419/research/index.md

15 KiB

phase agent agent_version generated_at concerns concerns_checked source_confidence workers_spawned
research research-advisor 2 2026-06-18T03:18:28Z
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/구현 몫).
true high 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 bibimbapSET 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 클라이언트 토글로 보임) — 더미 리뷰/계정/게임 범위 밖이라 미조사.