bibimbap/.atp/work-session/20260618-104034/research/code-coupling.md

22 KiB

phase agent agent_version generated_at concerns concerns_checked source_confidence workers_spawned
research research-advisor 2 2026-06-18T11:05:00+09:00
동결/권위 영역: db/schema.sql 의 game_comments/game_likes/games/users 는 '비권위 복원본'(타입 추론). game_reviews 신규 DDL 작성 시 인접 타입을 권위로 신뢰 금지 — recruit_posts(권위) 스타일을 따를 것.
현존 게임 상세의 댓글/좋아요는 100% 클라이언트 localStorage 다(서버 미연동). W3-2 가 '분리'가 아니라 사실상 '서버 영속화 신설'을 포함함 — 요구사항 advisor 전제(이미 서버 댓글이 있다는 가정)가 있다면 깨짐.
JSP 두 종 header 혼재: 활성 header 는 views/header.jsp(커스텀 세션·BibimbapCsrf). jsp/fragments/header.jspf 는 Spring Security taglib(sec:authorize, _csrf.parameterName) 기반 미사용 잔재 — 신규 작업에서 절대 참조 금지.
true high 0

조사 결과 — W3-2 댓글/리뷰 분리 코드 결합점

주제

W3-2 (댓글/리뷰 분리) 설계·구현을 위한 11개 결합점 정밀 조사. 모든 발견은 file:line 근거. 본 산출물은 전부 프로젝트 내부 코드 1차 확인(확인됨). 외부 자료 미사용 → source_confidence: high.

조사 방식: parallel-explorer worker 미사용. 11개 포인트가 동일 소규모 코드베이스(~25 Java + 17 JSP)에 밀결합되어 있어 advisor 가 직접 Read/Grep 으로 전수 확인. 모든 항목 1차 출처 직접 확인됨.


포인트별 발견

포인트 1: 댓글 도메인 CRUD 전체 [확인됨]

  • Mapper: mapper/GameCommentsMapper.java
    • GameCommentData getGameComment(long id):13-25 (SELECT, WHERE id=#{id} AND is_delete IS NOT TRUE)
    • int addGameComment(GameCommentData):27-39 (INSERT game_id/nickname/content, @Options(useGeneratedKeys=true, keyProperty="id"))
    • int updateGameComment(GameCommentData):41-51 (nickname/content/deleted_at 갱신 + is_delete = CASE WHEN deletedAt IS NULL THEN false ELSE true END)
    • DELETE/list 메서드 없음. game_id 별 목록 조회 메서드도 없음(soft-delete 일괄은 GamesMapper 에 있음 — 포인트 8).
  • POJO: data/GameCommentData.java:5-61 — 필드: Long id, Long gameId, String nickname, String content, OffsetDateTime createdAt, OffsetDateTime deletedAt. (userId 없음 — 작성자 식별 컬럼 부재, nickname 만 있음)
  • 컬럼(db/schema.sql:99-108, 비권위): id bigint, game_id bigint NOT NULL REFERENCES games(id), nickname varchar(100), content text, created_at timestamptz DEFAULT now(), deleted_at timestamptz, is_delete boolean DEFAULT false. (요청서가 언급한 6컬럼 + is_delete 7개 전부 일치)
  • 컨트롤러 엔드포인트: 존재하지 않음. GameCommentsMapper 를 주입/호출하는 컨트롤러 없음(rg 확인). 즉 서버측 댓글 작성/조회/삭제 HTTP API 가 전무.
  • 서비스 계층: 없음(프로젝트 전체가 controller→mapper 직결 구조).
  • 작성/조회/삭제 흐름: 현재 댓글은 서버 미연동. game-detail.jsp 가 localStorage 로만 처리(포인트 2). GameCommentsMapper 는 사실상 orphan(유일 사용처: 게임 삭제 cascade 의 GamesMapper.softDeleteGameComments, 포인트 8).

포인트 2: 게임 상세 페이지 [확인됨]

  • 핸들러: controller/api/GameController.java:103-129 @GetMapping("/game/{id}") String gameDetail(long id, Model, HttpSession).
    • DB 게임 존재 시 addGameModel(model, game, sessionUserId(session)) → view "game-detail" (:104-109).
    • DB miss 시 GameCatalog 정적 폴백(:111-128).
  • model attribute(addGameModel :254-268): gameId, gameName, creator, likeCount, likeCountFormatted, creatorNote, gitUrl, webglUrl, webglFrameSrc, webglDeployPath, owner. owner = currentUserId != null && currentUserId.equals(game.getUserId()) (:267).
  • game-detail.jsp 댓글/좋아요 실제 코드(localStorage):
    • 좋아요 버튼 #game-like-btn HTML :730-735; 좋아요 JS 전부 localStorage(LIKE_KEY='bibimbap-game-liked') — getLikedMap :815-823, setLiked :825-832, localStorage.setItem :830, 클릭 핸들러 :904-908 (서버 POST/DELETE 없음).
    • 댓글 폼 #game-comment-form HTML :790-801 (textarea name="comment" maxlength=1000, 작성자 입력란 없음).
    • 댓글 JS 전부 localStorage(COMMENT_KEY='bibimbap-game-comments') — getComments :911-920, saveComments :922-930(localStorage.setItem :928), 렌더 :938-992, submit 핸들러 :994-1005(닉네임 하드코딩 DEFAULT_NICK='익명' :936, id=crypto.randomUUID).
    • 라인 근거: 요청서가 지목한 812(LIKE_KEY)/830(setItem)/913(getComments raw)/928(saveComments setItem) 전부 위치 확정.

포인트 3: 로그인/세션 인증 패턴 [확인됨]

  • 인증 방식: Spring Security 아님. 커스텀 HttpSession attribute 기반.
  • 로그인 컨트롤러: controller/api/UserController.java@PostMapping("/login") :122-168, /signup :65-120, /logout :170-179. 로그인 페이지(GET)는 WebMvcController.mainView switch case "login" :75-80.
  • 세션 저장(UserController.saveLoginSession :502-525): session.setAttribute("userId", user.getId())(Long), 그 외 id, displayName, email, avatarUrl, role, status, authProvider, authIdentityId, lastLoginAt, 그리고 account(LinkedHashMap 복제본). 로그인 시 request.changeSessionId() :160(세션 고정 방어).
  • 현재 로그인 사용자 얻는 코드(컨트롤러 공통 헬퍼, 3곳에 동일 복붙): sessionUserId(HttpSession)GameController:291-307, RecruitController:155-171, UserController:333-349. session.getAttribute("userId") 를 Number/String→Long 변환, 없으면 null. (WebMvcController 는 :118-127 변형 — null 대신 IllegalStateException throw.)
  • UserData 필드 전체(data/UserData.java:5-15): Long id, String displayName, String canonicalEmail, String avatarUrl, String role, String status, OffsetDateTime lastLoginAt, createdAt, updatedAt. id 타입 Long, role 타입 String(기본값 "USER", UserController:43 ROLE_USER="USER"). 운영자 role 명칭은 코드상 미정의(USER 만 발급됨) — 포인트 6 참조.
  • 비로그인 처리: 페이지는 redirect:/login(RecruitController:43-46, GameController:133-135, WebMvcController:82-83). AJAX/상태변경 API 는 401 UNAUTHORIZED + {status,message} JSON(GameController:57-59 등).

포인트 4: CSRF 현황 (설계 핵심 제약) [확인됨]

  • spring-security 의존: 없음. pom.xml 의존성 = web, mybatis-spring-boot-starter, postgresql, tomcat-embed-jasper, lombok, starter-tomcat(provided), starter-test(test) (pom.xml:53-87). starter-security 부재.
  • SecurityConfig / SecurityFilterChain / @EnableWebSecurity: 클래스 없음(rg 확인 0건).
  • CSRF 인프라: 커스텀 자체 구현 존재security/CsrfTokens.java:
    • SESSION_ATTRIBUTE="csrfToken" :12, HEADER_NAME="X-CSRF-Token" :13.
    • getOrCreate(HttpSession) :20-33(세션에 토큰 발급/재사용, Base64 32바이트).
    • isValid(HttpServletRequest) :35-49 — 헤더 X-CSRF-Token 우선, 없으면 파라미터 _csrf(:46). 세션 토큰과 .equals 비교.
    • errorBody() :51-56{status:403, message:"요청 보안 토큰이 유효하지 않습니다."}.
  • 기존 POST/AJAX 의 토큰 전달 방식:
    • 뷰 노출: theme-init.jsp:5-7<meta name="csrf-token" content="<%= HtmlUtils.htmlEscape(CsrfTokens.getOrCreate(session)) %>"> 출력. 모든 페이지가 theme-init.jsp 를 include 하므로 메타 토큰이 전역 제공됨.
    • JS 헬퍼: theme-init.jsp:19-31 window.BibimbapCsrf = {token():meta 읽기, headers(extra):extra+{'X-CSRF-Token':token}}.
    • form hidden: login.jsp:221 <input type="hidden" name="_csrf" value="<%= csrfToken %>">(signup.jsp 동일).
    • AJAX 사용례: login.jsp:283 BibimbapCsrf.headers(...), game-detail.jsp:850(삭제) 동일 패턴.
    • 서버 검증례: 모든 상태변경 핸들러 진입부 if(!CsrfTokens.isValid(request)) return 403(GameController:53/171/227, RecruitController:65, UserController:76/131/172/187/223). 테스트: test/.../UserControllerCsrfTest.java.
  • 설계 함의: 신규 댓글/리뷰 POST/DELETE 는 반드시 CsrfTokens.isValid(request) 게이트 + 클라이언트 BibimbapCsrf.headers() 사용. 새 CSRF 인프라 신설 불필요(재사용).

포인트 5: DDL/스키마 적용 방식 [확인됨]

  • 부트스트랩: db/schema.sql 가 전체 스키마. flyway/liquibase 없음(docs/usage/local-setup.md:139 "flyway/liquibase 가 없다").
  • 적용법:
    • Docker: db 컨테이너 최초 기동 시 db/schema.sqldocker-entrypoint-initdb.d 로 자동 1회 실행(dev 스키마 채움, live 는 빈 스키마). 재적용은 docker compose down -v 후 재기동(local-setup.md:149).
    • 호스트 로컬 PG: psql -f db/schema.sql 수동(local-setup.md:150).
  • 권위 패턴: docs/recruit-posts-ddl.sql(권위) 가 신규 테이블 표준 스타일. docs/security-hardening-ddl.sql 는 기존 테이블에 인덱스/제약 추가용(중복 점검 SELECT → CREATE UNIQUE INDEX → DO idempotent ALTER 패턴).
  • DbUpdateQueryGenerator: 테스트 test/.../DbUpdateQueryGeneratorTest.java + test/db/dev-to-live-update.sql 존재(surefire 에서 제외됨, pom.xml:130-133). dev→live 스키마 동기화 SQL 생성 용도로 보임 — 마이그레이션 자동화 도구 아님(테스트성).
  • 신규 테이블(game_reviews) 추가 절차: ① db/schema.sqlSET search_path TO dev 블록 내 CREATE TABLE 추가, ② 권위 DDL 파일을 docs/ 에 별도 작성(recruit-posts-ddl.sql 선례), ③ 기존 DB 적용용 idempotent ALTER 스크립트(security-hardening-ddl.sql 선례). 파일 위치: db/schema.sql + docs/*-ddl.sql.

포인트 6: 유사 게시판 패턴 — RecruitController (핵심 레퍼런스) [확인됨]

  • controller/RecruitController.java + mapper/RecruitPostsMapper.java.
  • 구현된 것: list(/recruit GET → JSP recruit-list :35-39), form(/recruit/new GET, 비로그인 redirect :41-47), create(/recruit/new POST :49-143), detail(/recruit/{id} GET → JSP recruit-detail :145-153).
  • create 패턴(댓글/리뷰가 그대로 따를 표준):
    1. @Transactional (:50)
    2. CSRF 우선 검증 if(!CsrfTokens.isValid(request)) 403 (:65-67)
    3. 로그인 검증 userId = sessionUserId(session); if null → 401 (:68-71)
    4. trimToNull/trimToEmpty 정규화 + 길이/허용값(Set.contains) 검증, 위반 시 400 BAD_REQUEST (:73-114)
    5. data POJO 세팅 후 mapper.add, 생성 id null 체크 → 500 (:132-135)
    6. 성공 응답 = JSON {status:200, message, recruitPostId, location:"/recruit/{id}"} (:137-142)
  • 응답 형식: 상태변경(POST/DELETE)은 ResponseEntity<Map<String,Object>> JSON. 조회(GET)는 view name String(JSP). redirect 는 비로그인 폼 접근시만.
  • 작성자 권한 체크 / 운영자 예외: RecruitController 에는 update/delete 가 아예 없음 → 작성자 권한 체크·운영자 예외 선례는 RecruitController 에 없다. 권한 체크 표준 선례는 GameController 다: if(!userId.equals(existing.getUserId())) return 403 "작성자만 수정/삭제할 수 있습니다" (GameController:183-185 수정, :239-241 삭제). 운영자(admin) 예외 분기는 코드 전체에 없음(role 비교 분기 부재). → W3-2 가 운영자 삭제를 요구하면 신설 영역(role="ADMIN" 등 명칭도 미정의, 포인트 3).
  • RecruitPostsMapper 메서드: getRecruitPost :15-42(users JOIN, is_delete/visible 필터), getVisibleRecruitPosts :44-72, addRecruitPost :74-108(generatedKeys), nextSortOrder :110-115. update/delete 매퍼 없음.

포인트 7: 좋아요 패턴 (참고) [확인됨]

  • mapper/GameLikesMapper.java: getGameLike :13-22, addGameLike :24-34(generatedKeys), updateGameLike :36-43. 삭제/중복방지/카운트 매퍼 없음.
  • data/GameLikeData.java:5-43: Long id, Long gameId, String userKey, OffsetDateTime createdAt. (좋아요 주체 식별이 userKey String — userId FK 아님)
  • 컬럼(db/schema.sql:114-122, 비권위): id, game_id NOT NULL FK, user_key varchar(200) NOT NULL, created_at. is_delete 없음(hard delete 설계, schema.sql:112 주석 명시).
  • 영속화/중복방지 현황: GameLikesMapper 를 호출하는 컨트롤러 없음(rg 0건). 좋아요도 댓글과 동일하게 서버 미연동 → game-detail.jsp localStorage(LIKE_KEY)만. 중복방지는 DB UNIQUE 제약 없음(schema 상). 즉 likes 도 orphan mapper.
  • 카운트 출처: games.like_count(GamesMapper.getGame:27 like_count AS likeCount) 정적 컬럼 — 실시간 game_likes COUNT 아님.

포인트 8: 게임 삭제 cascade [확인됨]

  • GameController.deleteGame :222-252 (@DeleteMapping("/game/{id}"), @Transactional).
    • 순서(:243-245): gamesMapper.softDeleteGameComments(id)gamesMapper.deleteGameLikes(id)gamesMapper.softDeleteGame(id).
  • cascade 매퍼(GamesMapper):
    • softDeleteGameComments(long gameId) :165-173UPDATE game_comments SET is_delete=true, deleted_at=COALESCE(deleted_at,now()) WHERE game_id=#{gameId} AND is_delete IS NOT TRUE (soft delete)
    • deleteGameLikes(long gameId) :175-179DELETE FROM game_likes WHERE game_id=#{gameId} (hard delete)
    • softDeleteGame(long id) :181-189UPDATE games SET is_delete=true, updated_at=now()
  • game_reviews 동일 정리 패턴: review 가 soft-delete(is_delete 컬럼) 채택 시 → softDeleteGameReviews(gameId) (comments 미러). hard-delete 채택 시 → deleteGameReviews(gameId) (likes 미러). 그리고 deleteGame :243-245 에 호출 한 줄 추가가 정확한 결합점.

포인트 9: JSP escape 패턴 [확인됨]

  • 서버 렌더 출력: org.springframework.web.util.HtmlUtils.htmlEscape(...) 를 scriptlet <%= %> 안에서 사용이 지배적 패턴.
    • recruit-detail.jsp: 전 사용자 데이터 <%= HtmlUtils.htmlEscape(role/projectName/summary/contact/description...) %> (:219-288).
    • game-detail.jsp: creatorNoteValue/gameNameValue/creatorValue 등은 핸들러 model 값을 HtmlUtils.htmlEscape 로 미리 감싼 변수로 출력(:9 likeCountFormattedValue 등, 본문 :703/721/771).
    • header.jsp:11 avatarUrl HtmlUtils.htmlEscape.
  • JSTL escape: <c:out value='${q}'/>(header.jspf:7 — 단 header.jspf 는 미사용 잔재). 활성 뷰는 scriptlet 방식.
  • 클라이언트 렌더: game-detail.jsp 댓글 렌더는 textContent 사용(:951 av.textContent, :958 nickEl.textContent, :969 p.textContent) — innerHTML 은 초기화용 listEl.innerHTML=''(:942)만. → 신규 서버연동 댓글/리뷰 클라이언트 렌더도 textContent 규약 유지 필수(CLAUDE.md 보안원칙 일치).
  • 신규 review JSP 출력 규약: 사용자 입력(평점 코멘트 등) 서버 렌더 시 HtmlUtils.htmlEscape, 클라이언트 동적 삽입 시 textContent.

포인트 10: MyBatis annotation 규약 [확인됨]

  • annotation-only(XML 매퍼 없음). @Mapper 인터페이스 + @Select/@Insert/@Update/@Delete 인라인 SQL(Java text block """).
  • generatedKey: @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id") (GameCommentsMapper:38, RecruitPostsMapper:107, GamesMapper:138, GameLikesMapper:33).
  • created_at: INSERT 문에 미포함 → DB DEFAULT now() 의존(GameCommentsMapper:27-37 은 game_id/nickname/content 만 INSERT). updated_at 갱신은 UPDATE 문에서 updated_at=now() 명시(GamesMapper:159).
  • 바인딩: 전부 #{} (예: #{id}, #{gameId}). ${} 동적 치환 사용처 없음(검색 0건). 다중 인자는 @Param("name")(GamesMapper:91/115, RecruitPostsMapper:42). 컬럼 alias 는 snake_case AS camelCase 로 POJO 매핑.
  • soft-delete 조회 규약: 모든 SELECT 가 is_delete IS NOT TRUE 필터 + users JOIN 시 u.is_delete IS NOT TRUE.

포인트 11: DB 종류·문법 [확인됨]

  • DB: PostgreSQL. driver org.postgresql.Driver, url jdbc:postgresql://localhost:5433/bibimbap?currentSchema=dev (src/main/resources/dev/db.properties). Docker: jdbc:postgresql://db:5432/...?currentSchema=${APP_SCHEMA:-dev} (docker-compose.yml:40). pom.xml:64-67 postgresql runtime.
  • 스키마 분리: dev / live 두 PostgreSQL schema(search_path). schema.sql:21-24 CREATE SCHEMA dev/live; SET search_path TO dev.
  • DDL 문법 관례(db/schema.sql, recruit-posts-ddl.sql): SERIAL/BIGSERIAL 미사용 → 명시적 CREATE SEQUENCE + bigint DEFAULT nextval('..._id_seq'::regclass) + ALTER SEQUENCE ... OWNED BY. 타임스탬프 timestamp with time zone(=timestamptz, OffsetDateTime 매핑). boolean is_delete DEFAULT false NOT NULL. FK bigint REFERENCES "table"("id"). ON DELETE CASCADE 미사용(앱레벨 cascade, 포인트 8). CHECK 제약 사용례 recruit_posts(schema.sql:156-161). 인덱스 CREATE INDEX IF NOT EXISTS ... WHERE(partial index).
  • id 타입: 전 테이블 bigint(POJO Long). like_count/sort_order 만 integer.

종합 판단

핵심 상위 패턴

  • 프로젝트는 controller→mapper 직결(서비스 계층 없음), 커스텀 세션 인증 + 커스텀 CsrfTokens(Spring Security 전무), MyBatis annotation + 명시 시퀀스 PostgreSQL, soft-delete(is_delete) 규약.
  • 상태변경 API 표준 시퀀스: @Transactional → CSRF 검증 → 로그인 검증 → 정규화/검증 → mapper → JSON 응답.

충돌·갭 (요구사항 전제 점검 — 프로토콜 충돌 시 조항)

  • GAP-1 (중대): "댓글/리뷰 분리"라는 표현은 기존에 서버 댓글이 존재함을 함의하나, 실제 댓글·좋아요 모두 서버 미연동 localStorage 다. GameCommentsMapper/GameLikesMapper 는 orphan(삭제 cascade 외 미사용). 따라서 W3-2 는 분리 이전에 댓글 서버 영속화 + 리뷰 신설 두 가지를 포함. 요구사항 advisor 가 '댓글은 이미 서버에 있고 리뷰만 떼낸다'를 전제했다면 깨짐.
  • GAP-2: game_comments 에 작성자 식별 컬럼 없음(nickname 만, userId FK 부재). 로그인 사용자 귀속·작성자 권한 삭제를 원하면 user_id 컬럼 신설 필요.
  • GAP-3: 운영자(admin) 예외 로직이 코드 전체에 부재. role 은 "USER"만 발급되며 ADMIN 명칭·분기 미정의. 운영자 댓글/리뷰 삭제 요구 시 전부 신설.
  • GAP-4: game_reviews 테이블·POJO·매퍼·DDL 전무(docs work-log 도 "review 테이블 없음" 명시). 평점 축(단일/다축) 미정 — work-log:C6 에서 W3-2 설계 종속으로 남겨둠.

권위 격상 전 검증 필요 항목

  • 없음(전 항목 1차 코드 확인). 단, db/schema.sql 의 game_comments/game_likes/games/users 타입·길이·기본값은 schema.sql 자체가 '비권위 추론값'으로 선언(주석 :4-11). 신규 game_reviews DDL 의 인접 타입을 이들에서 복사할 때는 recruit_posts(권위) 스타일을 기준으로 삼을 것. (source_confidence 는 '코드가 이렇게 되어있다'는 사실에 대해 high. '운영 DB 실제 타입'은 schema.sql 스스로 미확인 선언.)

설계 입력 요약 (a 재사용 / b 신설)

(a) 재사용할 현존 패턴

  • CSRF: CsrfTokens.isValid(request) 게이트 + 뷰 BibimbapCsrf.headers()/메타 csrf-token + hidden _csrf. (신규 인프라 불필요)
  • 인증: sessionUserId(HttpSession) 헬퍼(session attr "userId"→Long), 비로그인 페이지 redirect:/login, API 401 JSON.
  • 컨트롤러 골격: RecruitController.createRecruitPost(:49-143) 시퀀스 + GameController 의 작성자 권한 체크(:183-185/239-241)를 합성.
  • 응답 형식: ResponseEntity<Map<String,Object>> {status,message,...,location} (조회는 JSP view name).
  • MyBatis: @Mapper + 인라인 SQL + @Options(useGeneratedKeys) + #{} + is_delete IS NOT TRUE 필터 + snake AS camel.
  • 삭제 cascade: GameController.deleteGame(:243-245) 에 review 정리 한 줄 추가.
  • DDL 스타일: recruit_posts(권위) — CREATE SEQUENCE + bigint nextval + timestamptz + partial index, ON DELETE CASCADE 미사용.
  • JSP escape: 서버 HtmlUtils.htmlEscape, 클라이언트 textContent.
  • 스키마 적용: db/schema.sql + docs/*-ddl.sql(권위) + idempotent ALTER(security-hardening 선례).

(b) 신설해야 할 것

  • 게임 댓글 서버 API: GameComment(s) 컨트롤러 신설(list GET + create POST + delete) — 현재 전무. localStorage JS(game-detail.jsp:807-1009) → fetch 기반으로 교체.
  • GameCommentsMapper 확장: game_id 별 목록 SELECT, (soft)delete 메서드 — 현재 없음.
  • game_reviews 도메인 전체: 테이블 DDL(db/schema.sql + docs/game-reviews-ddl.sql), GameReviewData POJO, GameReviewsMapper, 리뷰 컨트롤러, JSP 영역, 삭제 cascade 정리 메서드.
  • 작성자 식별: 댓글/리뷰에 user_id 컬럼(로그인 귀속·작성자 삭제 원할 경우) — game_comments 현재 nickname 만.
  • (요구 시) 운영자 예외: role 명칭 정의 + 권한 분기 — 현재 전무.
  • (설계 종속) 평점 모델: 단일 평점 vs 다축 — W3-2 설계에서 확정 필요(work-log C6).

미해결

  • 운영 DB 실제 스키마: game_comments/game_likes/games 타입·제약은 schema.sql 가 비권위 선언. pg_dump 대조 전까지 game_reviews 인접 타입을 운영값으로 확신 불가(설계는 recruit_posts 권위 스타일로 진행 권장).
  • 평점 데이터 모델: 단일 점수/다축/리뷰수 정렬 의미 — W3-2 요구·설계 결정 사항(조사 범위 밖).
  • 운영자 role 명칭: ADMIN 등 명칭과 부여 경로 미정(코드상 USER 만 존재).
  • 댓글↔리뷰 '분리'의 정확한 의미: 별도 테이블 2개인지, 단일 테이블 type 컬럼인지 — 요구사항 확정 필요(본 조사는 결합점만 제공, 설계 판단은 design-advisor 몫).