MSG-459 축제·팝업 미션 영상을 대표 격자에 올리기¶
티켓: MSG-459 · 작성일: 2026-08-22 · 작성: prd-writer 상태: 확정 (2026-08-22)
1. 문제 상황¶
축제와 팝업 미션은 판정 범위가 여러 격자에 걸쳐 있고, 지금은 사용자가 그중 한 칸을 골라 영상을 올린다. 축제는 중심 좌표 기준 9×9, 즉 81칸이다[^1]. 같은 축제를 다녀온 사람들의 영상이 81곳에 흩어지고, 어느 칸이 그 축제를 대표하는지 화면도 서버도 알지 못한다.
행사방은 같은 문제를 이미 한 번 풀었다. 위치마다 대표 격자를 하나 정해 영상을 그 칸에만 저장하고, 위치에 연결된 어느 격자를 눌러도 같은 피드를 보여준다. 사용자는 격자를 고르지 않고 장소를 고른다.
미션도 같은 모양이어야 하는데 지금은 갈라져 있다. 사용자는 축제방에서는 장소를 고르고 미션에서는 격자를 고른다. 서버 쪽 격차가 더 크다. missions 테이블에는 대표 격자 컬럼 자체가 없고, 시더는 적재할 때 원본 좌표를 격자로 바꾼 뒤 좌표를 버린다.
2. 목적 · 목표¶
- 목적: 축제와 팝업 미션의 영상을 한 칸에 모아, 미션 하나가 지도에서 하나의 자리로 읽히게 한다. 사용자가 격자를 고르는 부담도 없앤다.
- 목표
- 사용자는 축제·팝업 미션에서 격자를 고르지 않고 영상을 올린다. 어디에 저장할지는 서버가 정한다.
- 같은 미션에 올라온 영상은 전부 같은 격자에 모인다.
- 기존 미션 판정과 스탬프 규칙은 그대로 동작한다. 이번 변경은 저장 위치를 정하는 방식만 바꾼다.
- 축제가 끝난 뒤에도 그 자리에서 무슨 축제였는지 볼 수 있다.
- 비목표
- 코스(COURSE) 미션. 여러 포토스팟을 도는 것이 미션의 내용이라 대표 격자 하나로 묶으면 미션 자체가 성립하지 않는다.
- 화면에 칩이 없는 세 유형(AREA·THEME·CONTINUOUS). 조회 경로가 없어 업로드 진입점도 없다.
- 일반 영상 업로드(
POST /api/videos)의 좌표 기반 격자 매핑. 그대로 둔다. - 이미 올라온 영상의 격자를 옮기는 소급 이동.
- 미션 영상 목록 화면의 개편. 목록 조회 경로는 이미 있다.
3. 기능 요구사항¶
| ID | 요구사항 | 우선순위 |
|---|---|---|
| FR-1 | 축제·팝업 미션은 각각 대표 격자 하나를 가지며, 그 값은 미션 데이터를 적재할 때 정해져 저장된다 | Must |
| FR-2 | 사용자는 미션을 지목해 영상을 올릴 수 있고, 요청에 좌표나 격자를 담지 않는다 | Must |
| FR-3 | 미션 경유로 올린 영상은 그 미션의 대표 격자에 저장된다. 사용자의 실제 위치는 저장 위치를 바꾸지 않는다 | Must |
| FR-4 | 코스·구역·테마·상시 유형은 미션 경유 업로드를 받지 않고, 요청이 오면 거절한다 | Must |
| FR-5 | 대표 격자는 그 미션의 판정 범위 안에 있어야 한다. 범위 밖 값은 저장 단계에서 막힌다 | Must |
| FR-6 | 미션 경유 업로드가 확정되면 기존 판정이 그대로 돌아 스탬프와 뱃지가 발급된다. 판정 규칙 자체는 바뀌지 않는다 | Must |
| FR-7 | 같은 업로드 요청을 다시 보내도 영상이 중복 생성되지 않는다 | Must |
| FR-8 | 기간이 지난 미션에는 새 영상을 올릴 수 없다. 무기간 미션은 이 제한을 받지 않는다 | Must |
| FR-9 | 비로그인 사용자는 미션 경유 업로드를 할 수 없다 | Must |
| FR-10 | 없는 미션을 지목하면 실패하고, 그 응답으로 미션의 존재 여부를 구분할 수 없다 | Should |
| FR-11 | 대표 격자 산출은 결정적이다. 같은 미션 데이터를 다시 적재해도 같은 값이 나온다 | Must |
| FR-12 | 미션 데이터를 다시 적재해도 이미 저장된 대표 격자와 이미 올라온 영상은 영향을 받지 않는다. 다만 재적재로 판정 격자 집합이 바뀌어 기존 대표 격자가 그 집합 밖이 된 경우는 예외로, 새 집합에서 다시 산출한다[^3] | Must |
| FR-13 | 미션 경유로 올린 영상도 도감 점령을 만든다. 점령되는 칸은 그 미션의 대표 격자다 | Must |
| FR-14 | 대표 격자 산출은 행사방과 같은 규칙을 쓴다. 유형이나 칸 수에 따라 규칙을 갈라 쓰지 않는다 | Must |
| FR-15 | 사용자는 격자를 지목해 그 격자가 대표 격자인 축제·팝업 미션을 조회할 수 있다. 기간이 끝난 미션도 함께 나온다 | Must |
| FR-16 | 종료된 미션은 이름과 기간과 영상 수를 담아 나오고, 진행 중인 것과 구분되게 표시할 재료를 응답이 제공한다 | Must |
| FR-17 | 영상이 올라온 축제·팝업 미션은 기간이 끝나도 조회할 수 있는 상태로 남는다 | Must |
| FR-18 | 미션 경유 업로드는 요청에 담긴 촬영 시각이 미션 기간 안일 때만 받는다. 기간 밖이면 거절한다[^4] | Must |
4. 비기능 요구사항¶
| 분류 | 요구사항 |
|---|---|
| 데이터 정합 | 대표 격자는 그 미션의 판정 격자 집합에 속한 값이어야 하고, 이 관계는 애플리케이션 검사가 아니라 저장 계층이 보장한다 |
| 데이터 정합 | 이미 발급된 스탬프는 이번 변경으로 회수되거나 재계산되지 않는다. 스탬프 비회수는 기존 규칙이다[^2] |
| 운영 | 마이그레이션 하나로 컬럼을 추가하고, 이미 적재된 축제·팝업 미션의 값은 같은 산출 규칙으로 채운다. 채우지 못한 미션이 남으면 그 미션은 미션 경유 업로드를 받지 않는다 |
| 운영 | 미션 적재는 상시 스케줄러 없이 수동 실행이다. 이번 변경도 그 실행에 얹히고, 실패해도 기존 미션은 유지된다 |
| 보안·인가 | 업로드는 로그인 필수다. 사용자 식별은 인증 토큰에서만 얻고 요청 본문으로 받지 않는다 |
| 성능 | 업로드 확정 경로에 미션 조회 한 번과 멱등 재시도 판정(잠금 한 문장과 확정 영상 조회 한 건 — 행사 업로드가 이미 같은 모양으로 지불하는 비용)이 늘어난다. 판정 쿼리 수는 기존과 같아야 한다 |
5. 시퀀스 다이어그램¶
6. 클래스 다이어그램¶
RepresentativeGridResolver는 행사방이 쓰는 기존 클래스다. 산출 규칙을 새로 만들지 않고 그대로 쓰되, 지금 위치가 event/seed 아래라 공용으로 옮길지는 스펙에서 정한다.
7. 변경 파일 목록¶
| 파일 | 변경 | Owner |
|---|---|---|
src/main/resources/db/migration/V{n}__mission_representative_grid.sql |
신규. missions에 대표 격자 컬럼 추가, 판정 격자 집합 소속을 보장하는 제약, 기존 축제·팝업 행 백필 |
- |
src/main/java/com/msg/fillmap/mission/entity/Mission.java |
수정. 대표 격자 필드와 산출값 반영 메서드 | B |
src/main/java/com/msg/fillmap/mission/seed/FestivalMissionSeeder.java |
수정. 적재 시 대표 격자 산출·저장 | B |
src/main/java/com/msg/fillmap/mission/seed/PopupMissionSeeder.java |
수정. 같음 | B |
| 미션 경유 업로드 엔드포인트를 받을 컨트롤러 | 신규 또는 기존 확장. MissionVideoController라는 이름을 새로 만들 수 없다 - com.msg.fillmap.video.controller.MissionVideoController가 이미 있어(MSG-390 미션 영상 목록) 같은 이름을 다시 쓰면 빈 이름이 충돌해 기동이 실패한다. 배치와 이름은 스펙이 정한다 |
B |
src/main/java/com/msg/fillmap/mission/controller/MissionController.java |
수정. 격자로 미션을 되짚는 조회 추가 (FR-15) | B |
src/main/java/com/msg/fillmap/mission/dto/MissionVideoUploadRequestDto.java |
신규 | B |
src/main/java/com/msg/fillmap/mission/service/MissionVideoService.java (+impl) |
신규. 미션 조회·유형·기간 검사 후 영상 저장 위임 | B |
src/main/java/com/msg/fillmap/mission/exception/MissionErrorCode.java |
수정. 유형 불가·기간 마감 등 실패 코드 추가 | B |
src/main/java/com/msg/fillmap/video/service/VideoServiceImpl.java |
수정. 좌표 대신 격자를 직접 받는 저장 경로 분리 | B |
src/main/java/com/msg/fillmap/event/seed/RepresentativeGridResolver.java |
이동 또는 참조. 미션과 공용으로 쓸 위치 결정은 스펙 몫 | B |
src/main/java/com/msg/fillmap/global/config/SecurityConfig.java |
수정 없음 예상. 쓰기 경로는 기본이 인증 필수 | B |
missions 스키마 현황은 V6 신설, V13 source, V14 source_key와 POPUP 유형, V31 메타데이터 8컬럼이다. 좌표 컬럼은 없다.
8. 확정 사항과 미해결 질문¶
확정 (2026-08-22 팀원 K)¶
- 대표 격자 산출은 행사방 규칙 그대로 쓴다 (
RepresentativeGridResolver의 3단: 홀수 직사각형 정중앙 → 운영자 지정 → 무게중심 최근접, 동률이면 남서 우선). 팝업은 판정 범위가 짝수 사각형인 경우가 많아(2×2가 66%) 무게중심 동률에서 남서 칸이 뽑히는데, 규칙을 하나로 두는 쪽을 택했다. 적재 시점 원본 좌표를 쓰는 대안은 기각했다. - 미션 경유 업로드는 도감 점령을 만든다 (FR-13). 행사방이 2026-08-20에 같은 방향으로 확정한 것을 따른다. SRS FR-MISSION-05의 "미션 완료가 가짜 점령을 만들지 않는다"는 문구는 정정이 필요하다. 그 요구가 막으려던 것은 스탬프 발급이 점령을 만드는 것이고, 이번 건은 영상 업로드가 점령을 만드는 것이라 성격이 다르다. SRS 갱신은 이 티켓 범위에 포함한다.
- 기존 격자 선택 업로드는 축제·팝업에서도 그대로 열어 둔다. 미션 격자는 축제 기간에만 미션이지 평소에는 그냥 동네다. 막으면 그 자리에 사는 사람의 일상 업로드까지 걸린다. "미션 영상"은 미션 경유로 올린 것만으로 정의한다.
- 종료된 축제·팝업을 대표 격자에서 되짚어 볼 수 있게 한다 (FR-15~17, 2026-08-22 팀원 K). 지금은 미션 조회가 활성 기간으로 걸러 끝난 축제가 화면에서 사라진다. 영상은 대표 격자에 남아 있는데 그것이 무슨 축제였는지 알 방법이 없다. 행사방이 격자 역조회로 지난 회차까지 돌려주는 것과 같은 모양으로 맞춘다.
- 저장 쪽에 새로 만들 것이 없다. 역방향 인덱스
idx_mission_grids_grid가 이미 있고, 대표 격자 컬럼이 생기면 조인 없이 컬럼 하나로 찾는다. 영상 목록은 기존 미션 영상 조회를 그대로 쓴다. - 보존은 성립하지만 근거가 처음 쓴 것과 다르다 (2026-08-22 정정). 처음에는 "FR-8이 기간 지난 업로드를 막으므로 미션 경유로 영상이 올라왔다는 것은 곧 스탬프가 찍혔다는 뜻"이라고 적었는데, FR-8이 보는 것은 업로드 시각이고 스탬프 판정이 보는 것은 촬영 시각이라 고리가 끊긴다. 축제 기간 중에 지난달 영상을 골라 올리면 업로드는 통과하고 스탬프는 안 찍혀, 영상이 있는 미션이 종료 정리에 지워질 수 있었다. 스펙이 촬영 시각 가드로 이 고리를 다시 이었다(
docs/spec/MSG-459.mdD-9). 결론은 그대로다 - 정리 규칙을 손대지 않아도 영상이 있는 미션은 남는다. - MSG-450과의 순서는 해소됐다. 그 티켓이 2026-08-22에 develop으로 머지됐다(
58d90e3). 이 작업은 그 위에서 시작한다.
남은 질문¶
- 종료된 미션과 지워진 미션의 응답 구분은 필요 없어졌다 (2026-08-22 해소). 지워지는 것은 아무도 다녀가지 않은 미션이라 그 대표 격자에는 영상도 없다. 사용자가 그 격자를 눌러 마주칠 일이 없으므로 두 실패를 나눌 실익이 사라졌다. 업로드 실패는 하나의 응답으로 묶는다.
- 화면 흐름을 확인했다 (2026-08-22, 아래 9절). 남은 질문은 없다.
[^4]: 요청에 담긴 촬영 시각. 서버는 영상 파일을 열어 촬영 시각을 확인하지 않는다. 클라이언트가 채워 보내는 값을 그대로 쓴다. 서버가 이 값에 하는 일은 두 가지인데 둘 다 값 자체를 대조하지는 못한다. 영상 도메인이 공통으로 거는 "업로드 시점보다 미래인가" 검사(MSG-278)와, 이 요구가 새로 거는 "미션 기간 안인가" 검사다. 앱이 갤러리 항목의 메타데이터에서 이 값을 채우는 것이 의도된 동작이지만 그것은 클라이언트 관례이지 서버가 보증하는 사실이 아니다. 그래서 이 요구는 위조를 막는 장치가 아니다. 요청을 직접 만들어 기간 안 시각을 적으면 통과한다. 이 요구가 실제로 하는 일은 업로드를 받는 판정과 스탬프를 주는 판정이 같은 값을 보게 만드는 것이다. 두 판정이 어긋나면 영상은 올라왔는데 스탬프가 없는 미션이 생기고, 그 미션은 기간이 끝날 때 지워져 FR-17이 깨진다. 사용자에게 보이는 효과는 하나다. 지난달에 찍은 갤러리 영상을 이번 축제의 영상으로 올리려 하면 거절된다.
[^1]: 판정 범위. 그 미션을 완료한 것으로 인정하는 격자들. 축제는 중심 좌표 기준 9×9이고 팝업은 좌표 사방 40m가 걸치는 칸이다. 표시할 크기가 아니라 인정할 범위다. [^3]: FR-12 예외 (2026-08-22 정정). 처음 이 요구를 쓸 때 막으려던 것은 "같은 데이터를 다시 넣었는데 결과가 달라지는 일"이었다. 그런데 팝업이 실제로 자리를 옮기면 판정 격자 집합 자체가 바뀌고, 그때 옛 대표 격자를 고수하면 두 가지가 어긋난다. 저장 계층에서는 대표 격자가 판정 격자 집합에 속해야 한다는 제약이 깨져 재적재 트랜잭션 전체가 되돌아가고(그 주의 팝업이 통째로 노출되지 않는다), 사용자에게는 팝업이 없는 자리가 그 팝업의 영상이 모이는 자리로 남는다. 그래서 집합이 바뀐 경우에 한해 다시 산출한다. 집합이 그대로면 값도 그대로이므로 멱등이라는 원래 의도는 유지된다. 이 예외가 남기는 부작용(이미 올라온 영상은 옛 격자에 남고, 그 사이 재시도가 거절될 수 있다)은 스펙 D-3과 D-6에 한계로 기록돼 있다. [^2]: 비회수. 한 번 발급한 스탬프는 조건이 나중에 무너져도 회수하지 않는 규칙. 영상을 지워 조건이 미달이 돼도 스탬프는 남는다. 도감 점령이 영상 삭제로 롤백되는 것과 의도적으로 다르다.
9. 디자인 확인 (2026-08-22)¶
피그마 Fill Map 파일에서 앱 미션 화면을 확인했다. 미션 화면이 실린 곳은 CLAUDE.md가 정본으로 적어 둔 필맵 앱 디자인 MVP ver 5가 아니라 그 뒤에 만들어진 필맵 앱 디자인 MVP ver 6 페이지이고, 프레임 이름이 전부 v2로 끝나는 묶음이다. 정본 표기 갱신은 이 티켓 밖이라 사실만 적어 둔다.
확인한 흐름¶
지도 홈 상단 칩(지역축제·팝업스토어)을 누르면 아래에서 카드 목록이 올라온다(지도 홈(지역축제 · 칩 선택 v2)). 카드 하나를 누르면 미션 상세 시트로 바뀐다(지도 홈(지역축제 · 축제 선택 v2)의 bottom-sheet(축제 미션 상세), 노드 14851:21218). 팝업도 같은 구조다.
미션 상세 시트에 들어 있는 것은 이렇다.
- 제목과 남은 날짜 뱃지:
2026 통영한산대첩축제D-3 - 대표 사진, 그 아래
경남 통영시 정량동 · 8.11~8.16 - 완료 규칙 한 줄:
영역 안 아무 칸이나 1칸 채우면 완료 - 통계 세 칸:
내 진행 0/1칸·축제 기간 8.11~8.16·올라온 영상 2개 이 미션의 영상 2개 · 최신순아래로 영상 목록
이 티켓에 주는 결론 두 가지¶
첫째, 격자를 고르는 단계가 화면에 이미 없다. 사용자는 칩에서 카드로, 카드에서 미션 상세로 간다. 중간에 칸을 집는 화면이 없고 미션 상세도 칸을 나열하지 않는다. FR-2가 요청에서 좌표와 격자를 빼기로 한 것은 화면이 이미 그렇게 그려져 있다는 뜻이라 서버 계약과 어긋나지 않는다. 영상 목록이 격자별이 아니라 이 미션의 영상으로 묶여 있는 것도 한 칸에 모으는 이번 변경과 같은 방향이다.
둘째, 미션 상세에는 아직 업로드 버튼이 없다. 같은 파일의 격자 상세 시트(지도 홈(핫구역 · 칸 선택 v2))에는 영상 추가 · 공유 · 저장 액션 줄이 통계 바로 아래에 있는데, 미션 상세 시트는 그 자리에 통계 세 칸이 들어가 있고 액션 줄이 없다. 지금 화면에서 영상을 올리는 길은 탭바 가운데 카메라 버튼 하나뿐이고, 그 흐름의 마지막 화면(업로드 미리보기)은 위치를 부산 부산진구 서면 · 서면 A-14처럼 좌표에서 얻은 격자 이름으로 보여 준다. 미션을 지목하는 자리가 없다.
이것은 서버 범위를 바꾸지 않는다. 미션 경유 업로드 API는 그대로 만들면 되고, 그 API를 부를 진입점이 화면에 아직 없을 뿐이다. 디자인·FE 쪽에 필요한 것은 미션 상세 시트에 영상 추가를 다는 일 하나이며, 격자 상세 시트에 이미 같은 버튼이 있으므로 새로 만드는 모양이 아니라 옮겨 붙이는 모양이다. 이 요청은 이 티켓의 산출물이 아니라 FE·디자인 레인으로 넘긴다.