메이저맵
경력근거: 결제와 구독, 전환 퍼널 등을 실험 기반으로 유저가 가치를 느끼고 지갑을 여는 흐름 전체를 설계하고 구현합니다.
※ 실제 산출물이 아닌 AI 예측 시안입니다.
메이저맵은 학생·교사·학부모·대학이 함께 쓰는 진로 탐색 서비스를 만들어 왔고, 이제 확보한 유저에게 가치를 주면서 스스로 굴러가는 수익 구조를 세우는 일을 가장 중요한 문제로 내걸었습니다. 학과 추천과 전형 상세처럼 학년도마다 내용이 바뀌는 콘텐츠를 요금제로 여닫는 구조는 입시 일정을 따라가는 메이저맵의 제품에서만 나오고, 3월 경계로 학년도가 넘어가면서 같은 콘텐츠의 포함 여부가 바뀌는 자리가 잠금이 어긋나는 지점입니다. 다만 이 포트폴리오는 구독 권한과 실제 잠금 상태를 견주는 규칙과 그 결과를 읽는 화면을 보여줍니다. 결제 대행 연동이나 정산 실무 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
공고는 스택을 가리지 않고 수익화 제품을 만들며 결제와 구독, 전환 퍼널을 설계하고 구현한다고 적었습니다. 구독으로 열리는 콘텐츠가 실제로 그 학생에게만 열렸는지가 그 흐름의 첫 관문입니다.
확보한 유저에게 진짜 가치를 주면서 지속 가능한 온라인 비즈니스 모델을 만드는 일이 지금 가장 중요한 문제라고 밝혔습니다. 잠금이 어긋난 열람은 그 모델이 새는 자리입니다.
가설을 세우고 실험을 설계하고 결과를 읽어 다음 결정을 내리는 루프를 함께 돌린다고 적혀 있습니다. 요금제 표를 바꿔 넣고 지난 열람 기록에 다시 돌려 보는 2주차가 그 루프에 그대로 붙습니다.
학생이 유료 콘텐츠를 연 기록에 그 시각의 구독 줄과 요금제 표를 붙여, 열린 것이 맞는지 잠긴 것이 맞는지 열람 한 건씩 정합니다. 학생마다 구독 이력이 여러 줄이라 열람 시각보다 앞서 시작한 가장 늦은 줄을 골라 쓰고, 요금제 표도 학년도마다 줄이 달라 열람 시각을 3월 1일 경계로 학년도로 바꾼 뒤 그 시점에 효력이 있던 줄을 짚습니다. 무료로 열어 둔 콘텐츠 열람은 판정 대상이 아니라 목록에서 뺍니다. 값을 낸 학생인데 막힌 열람과, 안 낸 학생인데 열린 열람이 사유별로 나뉘어 환불 문의보다 먼저 손에 들어옵니다.
메이저맵 유료 전환과 학생 문의를 함께 보는 운영 담당자
잠금이 어긋난 열람은 결제 시스템이 아니라 요금제 표와 구독 기간이 어긋나는 자리에서 생깁니다. 열람 시각을 학년도로 바꿔 그때 효력이 있던 요금제 줄을 짚고 구독 기간과 실제 잠금 상태를 견주면, 환불 문의로 돌아올 열람과 매출이 새는 열람이 판정 다섯 갈래로 나뉩니다.
열람 기록과 요금제 표, 구독 원장을 올리면 열람 한 건마다 다섯 판정 중 하나가 붙고 사유별로 묶여 나오는 관리자 페이지입니다. 열람 기록은 공개된 서비스 화면 구조에서 뽑은 콘텐츠 코드로 지원자가 직접 만들어 넣고, 결제 대행 연동은 다루지 않습니다.
열람 320건에 손으로 매긴 정답 판정을 붙여 자동 판정과 대조하고 사유별 일치율을 냅니다. 2주차부터 GCP 로 배포해 운영 담당자가 사내 주소로 들어와 씁니다.
요금제 표를 새 안으로 갈아 끼운 뒤 지난 열람 기록에 다시 돌려, 어느 콘텐츠에서 잠김이 늘고 어느 쪽이 열리는지 두 안을 나란히 보여주는 단계입니다.
운영 담당자가 열람 기록과 요금제 표, 구독 원장을 올리면 잠금이 어긋난 열람이 사유별로 묶여 화면에 나오는 과정을 시연합니다.
열람 320건에 손으로 매긴 정답 판정을 붙여 대조하고 사유별 일치율을 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
망가진 입력 30종을 먼저 넣어 봅니다. 만료일이 비었거나 요금제 표에 줄이 없는 콘텐츠가 들어오거나 열람 시각이 구독 시작보다 앞서도 화면이 서고, 견주지 못한 조건은 판정 자리 대신 hedge 로 내려갑니다.
학년도 경계를 3월 1일에서 1월 1일로 옮겼을 때 판정이 몇 건 뒤집히는지 회귀로 확인합니다.
이 앞까지가 잰 값입니다. 일치율은 제가 만든 열람 기록에 제가 매긴 정답을 붙여 나온 숫자이고, 환불 문의가 실제로 줄었는지는 메이저맵의 실 열람 로그와 실 문의 기록을 넣어야 나옵니다. 그 측정을 붙이자마자 돌릴 수 있게 평가 러너와 정답 서식을 미리 갖춰 둡니다.
사유별 일치율은 제가 만든 기록을 제가 채점한 값이라 성능 근거가 못 됩니다. 면접에서는 화면이 도는 범위와, 실 열람 로그가 오면 바로 재볼 수 있다는 준비 상태까지만 말합니다.
요금제 표도 정답 판정도 제가 적었으니 맞아떨어지는 것이 당연합니다. 만료일이 비어 있는 체험 구독과 요금제 표에 줄이 없는 콘텐츠, 구독 시작보다 앞선 열람을 일부러 섞어 두어 깨끗한 기록만 통과시키는 화면이 되지 않게 했습니다.
이 포트폴리오는 구독 권한과 실제 잠금 상태를 견주는 규칙과 화면을 보여줍니다. 결제 대행 연동이나 환불 정산의 깊이는 주장하지 않습니다. 판정은 기간 비교와 표 조회가 하고 모델은 담당자가 학생에게 보낼 문장만 씁니다.
열람 시각에 아직 구독이 시작되지 않은 학생의 열람은 판정 다섯 가지가 나누지 못합니다. 화면이 잠겨 있었으면 맞게 열림으로 닫히는데 견줄 요금제 줄이 없어 근거가 비므로, 그 열람은 hedge 로 표시해 사람이 보게 남겼습니다(v-70910). 결제한 뒤 구독 이력이 늦게 넘어와 이렇게 보이는 경우가 섞여 있어, 실제 도입에서는 결제 시각과 구독 시작 시각을 함께 받아야 둘이 갈립니다.
실제 도입 때는 요금제가 개편되고 콘텐츠도 계속 늘어납니다. 학년도 경계일과 요금제 개편 이력을 코드에서 빼 설정으로 옮기는 일을 운영 과제로 둡니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
학생이 유료 콘텐츠를 연 기록입니다. 판정의 대상 표이며, 유입 화면이 남지 않은 열람과 구독 시작보다 앞선 열람을 일부러 담았습니다. 3월 1일 학년도 경계를 사이에 두고 같은 학생이 같은 콘텐츠를 이틀 차이로 두 번 연 쌍도 담아, 경계일을 옮기면 판정이 뒤집히는 자리를 데이터로 짚을 수 있게 했습니다.
| view_id | student_id | content_code | viewed_at | was_locked | entry_source |
|---|---|---|---|---|---|
| v-70910 | s-31077 | FIT_TEST_DEEP | 2026-02-18T18:05:00 | true | 찰떡테스트 결과 |
| v-71012 | s-31902 | FIT_TEST_DEEP | 2026-02-28T21:10:00 | true | 찰떡테스트 결과 |
| v-71048 | s-31902 | FIT_TEST_DEEP | 2026-03-02T20:05:00 | true | 찰떡테스트 결과 |
| v-70988 | s-30188 | FIT_TEST_DEEP | 2026-02-10T19:30:00 | true | 찰떡테스트 결과 |
| v-71204 | s-30188 | MAJOR_REC_FULL | 2026-09-09T20:14:00 | true | 검색결과 |
아래 JSON Schema 에 맞는 콘텐츠 열람 행을 더 만들어 주세요. {"type":"object","properties":{"view_id":{"type":"string"},"student_id":{"type":"string"},"content_code":{"type":"string","enum":["MAJOR_REC_FULL","ADMISSION_DETAIL_SUSI","FIT_TEST_DEEP","UNIV_CUTLINE_COMPARE","CAREER_ROADMAP"]},"viewed_at":{"type":"string"},"was_locked":{"type":"boolean"},"entry_source":{"type":["string","null"]}},"required":["view_id","student_id","content_code","viewed_at","was_locked","entry_source"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) entry_source 가 null 인 열람, (2) 구독 시작보다 앞선 시각의 열람, (3) 3월 1일 앞뒤로 며칠 차이 나는 열람 쌍, (4) plan_features 에 줄이 없는 콘텐츠 코드. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"view_id":{"type":"string"},"student_id":{"type":"string"},"content_code":{"type":"string","enum":["MAJOR_REC_FULL","ADMISSION_DETAIL_SUSI","FIT_TEST_DEEP","UNIV_CUTLINE_COMPARE","CAREER_ROADMAP"]},"viewed_at":{"type":"string"},"was_locked":{"type":"boolean"},"entry_source":{"type":["string","null"]}},"required":["view_id","student_id","content_code","viewed_at","was_locked","entry_source"]}요금제마다 어느 콘텐츠가 몇 학년도부터 포함인지 적은 기준 표입니다. 같은 콘텐츠가 학년도별로 여러 줄을 갖고, 판정은 열람 시각의 학년도에 효력이 있던 줄만 봅니다.
| plan_code | content_code | from_grade_year | is_included | feature_label |
|---|---|---|---|---|
| PLAN_FREE | CAREER_ROADMAP | 2,025 | true | 진로 로드맵 |
| PLAN_FREE | MAJOR_REC_FULL | 2,025 | false | 학과 추천 전체목록 |
| PLAN_FREE | ADMISSION_DETAIL_SUSI | 2,025 | false | 전형 상세 수시 |
| PLAN_FREE | FIT_TEST_DEEP | 2,025 | false | 찰떡테스트 심화해설 |
| PLAN_CAREER_PLUS | CAREER_ROADMAP | 2,025 | true | 진로 로드맵 |
아래 JSON Schema 에 맞는 요금제 기준 행을 더 만들어 주세요. {"type":"object","properties":{"plan_code":{"type":"string","enum":["PLAN_FREE","PLAN_CAREER_PLUS","PLAN_ADMISSION_PRO"]},"content_code":{"type":"string","enum":["MAJOR_REC_FULL","ADMISSION_DETAIL_SUSI","FIT_TEST_DEEP","UNIV_CUTLINE_COMPARE","CAREER_ROADMAP"]},"from_grade_year":{"type":"integer"},"is_included":{"type":"boolean"},"feature_label":{"type":"string"}},"required":["plan_code","content_code","from_grade_year","is_included","feature_label"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) 같은 요금제·같은 콘텐츠가 학년도별로 포함 여부가 뒤바뀌는 줄 쌍, (2) 무료 요금제에만 포함으로 적힌 콘텐츠, (3) 상위 요금제에는 있는데 하위 요금제에는 줄 자체가 없는 콘텐츠. 생성한 행은 반드시 위 스키마를 통과해야 하며, 한 요금제·콘텐츠·학년도 조합은 한 줄만 두세요.{"type":"object","properties":{"plan_code":{"type":"string","enum":["PLAN_FREE","PLAN_CAREER_PLUS","PLAN_ADMISSION_PRO"]},"content_code":{"type":"string","enum":["MAJOR_REC_FULL","ADMISSION_DETAIL_SUSI","FIT_TEST_DEEP","UNIV_CUTLINE_COMPARE","CAREER_ROADMAP"]},"from_grade_year":{"type":"integer"},"is_included":{"type":"boolean"},"feature_label":{"type":"string"}},"required":["plan_code","content_code","from_grade_year","is_included","feature_label"]}학생별 구독 원장입니다. 한 학생이 요금제를 갈아탄 이력이 여러 줄로 남고, 만료일이 아직 정해지지 않은 체험 구독을 담아 만료 판정을 건너뛰는 경로를 보여줍니다.
| subscription_no | student_id | plan_code | started_at | expires_at |
|---|---|---|---|---|
| SUB-2025-0912 | s-30188 | PLAN_FREE | 2025-11-02T00:00:00 | 2026-05-31T00:00:00 |
| SUB-2026-0117 | s-30188 | PLAN_CAREER_PLUS | 2026-06-01T00:00:00 | 2026-12-01T00:00:00 |
| SUB-2026-0142 | s-29820 | PLAN_ADMISSION_PRO | 2026-03-02T00:00:00 | 2026-12-02T00:00:00 |
| SUB-2026-0166 | s-31902 | PLAN_CAREER_PLUS | 2026-02-28T00:00:00 | 2026-08-28T00:00:00 |
| SUB-2026-0181 | s-28744 | PLAN_ADMISSION_PRO | 2026-07-15T00:00:00 | 2027-01-15T00:00:00 |
아래 JSON Schema 에 맞는 구독 원장 행을 더 만들어 주세요. {"type":"object","properties":{"subscription_no":{"type":"string"},"student_id":{"type":"string"},"plan_code":{"type":"string","enum":["PLAN_FREE","PLAN_CAREER_PLUS","PLAN_ADMISSION_PRO"]},"started_at":{"type":"string"},"expires_at":{"type":["string","null"]}},"required":["subscription_no","student_id","plan_code","started_at","expires_at"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) expires_at 이 null 인 체험 구독, (2) 한 학생이 요금제를 갈아타 기간이 이어지는 두 줄, (3) 만료일이 지난 지 며칠 안 된 구독, (4) 시작일이 미래인 예약 구독. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"subscription_no":{"type":"string"},"student_id":{"type":"string"},"plan_code":{"type":"string","enum":["PLAN_FREE","PLAN_CAREER_PLUS","PLAN_ADMISSION_PRO"]},"started_at":{"type":"string"},"expires_at":{"type":["string","null"]}},"required":["subscription_no","student_id","plan_code","started_at","expires_at"]}| Python | 열람 시각을 학년도로 바꾸고 효력 있는 요금제 줄을 고른 뒤 판정 차례를 태우는 규칙 엔진과 평가 러너의 언어입니다. |
| Next.js | 사유별로 묶은 열람 목록과 열람 하나를 펼쳐 보는 상세를 그리는 관리자 페이지입니다. |
| PostgreSQL | 열람 기록·요금제 표·구독 원장을 담는 저장소입니다. 요금제 표에 (plan_code, content_code, from_grade_year) 유일 제약을 걸어 한 학년도에 효력 줄이 둘 생기지 않게 합니다. |
| GCP | 2주차부터 배포와 운영을 맡습니다. 판정 결과와 올린 원본을 Cloud Storage 에 남기고 Cloud SQL 로 PostgreSQL 을 띄웁니다. |
| LLM | 판정이 정해진 뒤 학생 문의에 그대로 회신할 한 문장을 씁니다. |
✗ fail → 요금제 표에 줄이 없는 콘텐츠가 절반을 넘으면 기획 쪽에서 그 표부터 받아 채웁니다.
✓ pass → 2주차 정답 대조와 GCP 배포로 넘어갑니다.
✗ fail → 어긋난 열람을 모아 효력 줄 고르는 기준과 판정 차례를 다시 봅니다.
✓ pass → 3주차 요금제 갈아 끼우기로 넘어갑니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
verdict사유 라벨 · view_id + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
맞게 열림맞게 열림낸 학생인데 잠김결제했는데 잠김안 낸 학생인데 열림결제 없이 열림요금제에 없는 콘텐츠요금제 표에 없음만료 뒤에도 열림만료 뒤 열림유료 콘텐츠 열람 한 건이 구독 줄·요금제 줄과 견줘 어떤 판정을 받는지 남기는 형식입니다. 판정은 규칙이 정하고 회신 문안만 모델이 씁니다.
{"type":"object","properties":{"verdict_no":{"type":"string"},"view_id":{"type":"string"},"student_id":{"type":"string"},"content_code":{"type":"string","enum":["MAJOR_REC_FULL","ADMISSION_DETAIL_SUSI","FIT_TEST_DEEP","UNIV_CUTLINE_COMPARE","CAREER_ROADMAP"]},"verdict":{"type":"string","enum":["맞게 열림","낸 학생인데 잠김","안 낸 학생인데 열림","요금제에 없는 콘텐츠","만료 뒤에도 열림"]},"view_grade_year":{"type":"integer","description":"viewed_at 이 그 해 3월 1일 이후면 그 해, 앞이면 전 해입니다. 학년도가 바뀌는 자리를 이 계산 하나로 맞춥니다."},"matched_subscription_no":{"type":["string","null"],"description":"그 학생의 subscriptions 중 started_at 이 viewed_at 보다 앞선 줄에서 started_at 이 가장 늦은 하나입니다. 그런 줄이 없으면 비웁니다."},"effective_from_grade_year":{"type":["integer","null"],"description":"그 요금제와 콘텐츠의 plan_features 줄 가운데 from_grade_year 가 view_grade_year 이하인 것 중 가장 큰 값입니다. 해당 줄이 없으면 비웁니다."},"plan_included":{"type":["boolean","null"],"description":"effective_from_grade_year 가 가리키는 줄의 is_included 값입니다. 줄을 고르지 못하면 비웁니다."},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{}},"required":["field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["verdict_no","view_id","student_id","content_code","verdict","view_grade_year","matched_subscription_no","plan_included","evidence","summary"]}[
{
"verdict_no": "CV-0001",
"view_id": "v-71204",
"student_id": "s-30188",
"content_code": "MAJOR_REC_FULL",
"verdict": "낸 학생인데 잠김",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0117",
"effective_from_grade_year": 2025,
"plan_included": true,
"evidence": [
{
"field": "plan_code",
"value": "PLAN_CAREER_PLUS"
},
{
"field": "is_included",
"value": true
},
{
"field": "was_locked",
"value": true
},
{
"field": "expires_at",
"value": "2026-12-01T00:00:00"
},
{
"field": "viewed_at",
"value": "2026-09-09T20:14:00"
}
],
"summary": "진로플러스를 결제 중인 학생인데 학과 추천 전체목록이 잠긴 채로 떴습니다.",
"hedge": null
},
{
"verdict_no": "CV-0002",
"view_id": "v-71330",
"student_id": "s-29115",
"content_code": "ADMISSION_DETAIL_SUSI",
"verdict": "안 낸 학생인데 열림",
"view_grade_year": 2026,
"matched_subscription_no": null,
"effective_from_grade_year": null,
"plan_included": null,
"evidence": [
{
"field": "student_id",
"value": "s-29115"
},
{
"field": "was_locked",
"value": false
},
{
"field": "entry_source",
"value": "찰떡테스트 결과"
},
{
"field": "viewed_at",
"value": "2026-09-09T21:02:00"
}
],
"summary": "구독 원장에 없는 학생에게 전형 상세 수시가 잠금 없이 열렸습니다.",
"hedge": "결제가 다른 계정으로 잡혀 학생 번호가 갈라졌을 때도 같은 모양이 되므로 계정 병합 이력을 함께 봐야 합니다."
},
{
"verdict_no": "CV-0003",
"view_id": "v-71455",
"student_id": "s-31902",
"content_code": "FIT_TEST_DEEP",
"verdict": "만료 뒤에도 열림",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0166",
"effective_from_grade_year": 2026,
"plan_included": true,
"evidence": [
{
"field": "expires_at",
"value": "2026-08-28T00:00:00"
},
{
"field": "viewed_at",
"value": "2026-09-09T22:40:00"
},
{
"field": "was_locked",
"value": false
},
{
"field": "entry_source",
"value": null
}
],
"summary": "8월 28일에 끝난 구독인데 찰떡테스트 심화해설이 9월 9일에도 열렸습니다.",
"hedge": null
},
{
"verdict_no": "CV-0004",
"view_id": "v-71512",
"student_id": "s-28744",
"content_code": "UNIV_CUTLINE_COMPARE",
"verdict": "요금제에 없는 콘텐츠",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0181",
"effective_from_grade_year": null,
"plan_included": null,
"evidence": [
{
"field": "content_code",
"value": "UNIV_CUTLINE_COMPARE"
},
{
"field": "plan_code",
"value": "PLAN_ADMISSION_PRO"
},
{
"field": "was_locked",
"value": false
},
{
"field": "viewed_at",
"value": "2026-09-09T23:11:00"
}
],
"summary": "대학별 합격선 비교가 어느 요금제 표에도 서 있지 않아 열림과 잠김을 판단할 잣대가 없습니다.",
"hedge": "새로 붙인 콘텐츠가 요금제 표에 아직 안 올라왔을 때 이렇게 걸리므로 기획 쪽에 그 줄부터 요청해야 합니다."
},
{
"verdict_no": "CV-0005",
"view_id": "v-71260",
"student_id": "s-29820",
"content_code": "ADMISSION_DETAIL_SUSI",
"verdict": "맞게 열림",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0142",
"effective_from_grade_year": 2025,
"plan_included": true,
"evidence": [
{
"field": "plan_code",
"value": "PLAN_ADMISSION_PRO"
},
{
"field": "is_included",
"value": true
},
{
"field": "was_locked",
"value": false
},
{
"field": "expires_at",
"value": "2026-12-02T00:00:00"
},
{
"field": "from_grade_year",
"value": 2025
}
],
"summary": "입시프로 구독 기간 안이고 요금제에 든 콘텐츠라 열린 것이 맞습니다.",
"hedge": null
},
{
"verdict_no": "CV-0006",
"view_id": "v-71377",
"student_id": "s-30455",
"content_code": "FIT_TEST_DEEP",
"verdict": "맞게 열림",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0193",
"effective_from_grade_year": 2026,
"plan_included": true,
"evidence": [
{
"field": "plan_code",
"value": "PLAN_CAREER_PLUS"
},
{
"field": "from_grade_year",
"value": 2026
},
{
"field": "was_locked",
"value": false
},
{
"field": "expires_at",
"value": null
},
{
"field": "entry_source",
"value": null
}
],
"summary": "2026 학년도부터 진로플러스에 든 콘텐츠라 열린 것이 맞습니다.",
"hedge": "만료일이 비어 있는 체험 구독이라 만료 여부는 따지지 않았고, 유입 화면 값도 없어 어디서 들어왔는지는 남지 않았습니다."
},
{
"verdict_no": "CV-0007",
"view_id": "v-71048",
"student_id": "s-31902",
"content_code": "FIT_TEST_DEEP",
"verdict": "낸 학생인데 잠김",
"view_grade_year": 2026,
"matched_subscription_no": "SUB-2026-0166",
"effective_from_grade_year": 2026,
"plan_included": true,
"evidence": [
{
"field": "plan_code",
"value": "PLAN_CAREER_PLUS"
},
{
"field": "from_grade_year",
"value": 2026
},
{
"field": "is_included",
"value": true
},
{
"field": "was_locked",
"value": true
},
{
"field": "viewed_at",
"value": "2026-03-02T20:05:00"
}
],
"summary": "3월 2일은 2026 학년도라 찰떡테스트 심화해설이 진로플러스에 든 콘텐츠인데도 잠긴 채로 떴습니다.",
"hedge": null
},
{
"verdict_no": "CV-0008",
"view_id": "v-71012",
"student_id": "s-31902",
"content_code": "FIT_TEST_DEEP",
"verdict": "맞게 열림",
"view_grade_year": 2025,
"matched_subscription_no": "SUB-2026-0166",
"effective_from_grade_year": 2025,
"plan_included": false,
"evidence": [
{
"field": "plan_code",
"value": "PLAN_CAREER_PLUS"
},
{
"field": "from_grade_year",
"value": 2025
},
{
"field": "is_included",
"value": false
},
{
"field": "was_locked",
"value": true
},
{
"field": "viewed_at",
"value": "2026-02-28T21:10:00"
}
],
"summary": "이틀 앞선 2월 28일은 아직 2025 학년도라 같은 콘텐츠가 진로플러스에 들지 않아 잠긴 것이 맞습니다.",
"hedge": "학년도 경계일을 1월 1일로 옮기면 이 열람은 낸 학생인데 잠김으로 뒤집힙니다."
},
{
"verdict_no": "CV-0009",
"view_id": "v-70910",
"student_id": "s-31077",
"content_code": "FIT_TEST_DEEP",
"verdict": "맞게 열림",
"view_grade_year": 2025,
"matched_subscription_no": null,
"effective_from_grade_year": null,
"plan_included": null,
"evidence": [
{
"field": "student_id",
"value": "s-31077"
},
{
"field": "viewed_at",
"value": "2026-02-18T18:05:00"
},
{
"field": "started_at",
"value": "2026-04-10T00:00:00"
},
{
"field": "was_locked",
"value": true
},
{
"field": "content_code",
"value": "FIT_TEST_DEEP"
}
],
"summary": "2월 18일 열람 시각에는 이 학생의 구독이 아직 시작되지 않았고 화면도 잠겨 있어 열리지 않은 것이 맞습니다.",
"hedge": "견줄 구독 줄과 요금제 줄이 없어 근거가 비어 있습니다. 결제 뒤에 구독 이력이 늦게 적재된 경우도 같은 모양이라 사람이 확인해야 합니다."
}
][자동 검증 · 코드가 판정합니다] 1. verdict 가 다섯 값 중 하나인지 확인합니다. 규칙이 정한 값이라 모델이 새 값을 지어내면 그 응답을 버립니다. 2. verdict_no·view_id·student_id·content_code·verdict·view_grade_year·evidence·summary 필드가 빠짐없이 존재하는지 확인합니다. 3. view_grade_year 가 2020 이상 2030 이하 범위 안인지 확인합니다. 4. view_grade_year 가 viewed_at 에서 3월 1일을 경계로 다시 계산한 값과 같은지 확인합니다. 5. 낸 학생인데 잠김은 그 학생의 구독 줄에서 고른 plan_code 로 content_code 의 plan_features 줄을 짚어 is_included 가 참인데 was_locked 도 참인지 다시 계산합니다. 6. 안 낸 학생인데 열림은 matched_subscription_no 가 비었는데 was_locked 가 거짓인지 다시 계산합니다. 구독 원장에 학생 번호가 적혀 있는지가 아니라 열람 시각보다 앞서 시작한 구독 줄이 그 학생에게 하나라도 있는지로 가릅니다. 한 학생의 결제 전 열람과 결제 뒤 열람이 같은 번호로 섞여 들어오므로, 원장에 이름이 있다는 이유로 결제 전 열람이 이 사유를 비껴가면 값을 받지 않고 내준 열람을 놓칩니다. 7. 요금제에 없는 콘텐츠는 그 content_code 가 plan_features.content_code 어디에도 없는지 다시 계산합니다. 8. 만료 뒤에도 열림은 viewed_at 이 그 구독 줄의 expires_at 보다 뒤인데 was_locked 가 거짓인지 다시 계산합니다. 다만 expires_at 이 비어 있으면 만료 여부를 따지지 않고 그 사실을 hedge 에 남깁니다. 9. evidence 의 field 이름이 입력에 있는 컬럼인지 대조합니다. 없는 이름을 인용하면 회신 문안이 지어낸 문장이라는 뜻이라 규칙이 만든 기본 문장으로 되돌립니다. 10. evidence 의 value 가 수록된 행의 원문 값과 같은지 확인합니다. 11. summary 가 존댓말 한 문장으로 끝나는지 확인합니다. 12. 무료 요금제 줄이 포함으로 적힌 콘텐츠(진로 로드맵)의 열람은 잠금 판정 대상이 아니라 목록에 넣지 않습니다. v-71566 이 그 자리라 기대 출력이 없습니다. 13. 학년도 경계는 v-71012 와 v-71048 한 쌍으로 확인합니다. 같은 학생이 같은 콘텐츠를 이틀 사이에 두 번 열었는데 2월 28일은 2025 학년도라 진로플러스에 들지 않은 콘텐츠이고, 3월 2일은 2026 학년도라 든 콘텐츠입니다. 경계일을 1월 1일로 옮기면 앞의 열람이 맞게 열림에서 낸 학생인데 잠김으로 뒤집히고, metrics 의 경계일 회귀가 세는 것이 이 한 쌍입니다. 14. matched_subscription_no 와 plan_included 가 둘 다 비었는데 앞의 네 갈래에 하나도 안 걸린 열람은 맞게 열림으로 닫되, 견줄 구독 줄과 요금제 줄이 없었다는 사실을 hedge 에 남겨 사람이 보게 합니다. v-70910 이 그 자리입니다 — 이 학생의 구독은 4월 10일에 시작해 2월 18일 열람 시각에는 아직 없었고, 화면이 잠겨 있어 안 낸 학생인데 열림에도 걸리지 않습니다. 결제 전 열람이 제대로 잠겨 있던 경우와 구독 이력이 늦게 적재된 경우가 같은 모양이라 근거 없이 통과시키지 않습니다. [평가 러너 · 2주차] 손으로 매긴 정답 판정 320건과 자동 판정을 대조해 사유별 일치율을 내고, 어긋난 열람은 따로 모아 효력 줄 고르는 기준을 손볼지 정합니다. 요금제 표를 새 안으로 갈아 끼웠을 때 판정이 어떻게 움직이는지도 지난 열람 기록에 다시 돌려 이 러너에서 봅니다. [수동 검증 · 사람이 봅니다] 1. 낸 학생인데 잠김으로 걸린 열람 20건을 학생 문의 기록과 맞춰 실제 환불 요청으로 이어졌는지 봅니다. 2. 안 낸 학생인데 열림 중 계정이 갈라진 사례가 있는지 판단해 판정 차례를 다시 봅니다.