비바시티
경력근거: 수집 실패 감지, 원본 대조, 정합성 검증, 재처리를 나중에 붙이는 기능이 아니라
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고는 레거시 이관을 최우선 과제로 두고 서비스 중단과 데이터 손실을 최소화하는 이관 방식을 설계하라고 하며, 원본 대조와 정합성 검증을 나중에 붙이는 기능이 아니라 기본 아키텍처의 일부로 만들라고 적습니다. 이 포트폴리오는 그 문장을 그대로 만든 것입니다. 이관 단계마다 대조 결과가 남아 판단의 근거가 되고, 재처리 대상이 목록으로 나옵니다. 다만 이 포트폴리오는 대조 규칙과 운영 화면 구현을 보여줍니다. 게임 커뮤니티 분석이나 다국어 번역 품질에 대한 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
현재 백엔드와 데이터 파이프라인의 구조를 파악해 목표 아키텍처와 단계별 이관 계획을 세우고, 서비스 중단과 데이터 손실을 최소화하는 이관 방식을 설계하라고 첫 업무로 적습니다.
수집 실패 감지와 원본 대조와 정합성 검증과 재처리를 나중에 붙이는 기능이 아니라 기본 아키텍처의 일부로 만들라고 못박습니다. 검증이 부가 기능이 아니라 골조입니다.
기술적 결정의 이유와 배경을 기록으로 남기는 방식을 정착시키고 팀이 같은 방향으로 개발하게 만드는 일을 업무에 둡니다. 결과만이 아니라 판단의 근거가 남아야 하는 자리입니다.
이관을 맡은 개발자가 대조 대상을 고르면, 원본 테이블과 새 구조로 옮긴 테이블을 업무 키로 맞춰 빠진 행과 값이 달라진 행을 나눠 보여줍니다. 어긋났다는 판정은 항목별 비교 규칙이 정하고, 모델은 비교한 두 값을 들어 어느 항목이 어떻게 달라졌는지 설명합니다.
저는 수집 파이프라인을 새 구조로 옮기고 있습니다. 배치가 끝나면 옮긴 건수만 로그에 남는데, 번역 필드가 통째로 비어 들어간 걸 나중에 화면에서 발견했습니다. 어디까지 믿고 다음 테이블로 넘어가도 되는지 판단할 근거가 없습니다.
이관이 불안한 이유는 옮기는 코드가 어려워서가 아니라 옮긴 결과를 원본과 맞춰 볼 자리가 없기 때문입니다. 업무 키로 두 쪽을 맞춰 빠진 행과 값이 달라진 행을 나눠 주면, 이관을 멈출지 넘어갈지가 수치로 정해집니다.
이관 담당자가 쓰는 화면 하나가 완결됩니다. 원본과 옮긴 결과를 업무 키로 맞춰 빠진 행·중복 행·값이 달라진 행을 나눠 보여주고, 판정 근거가 된 두 값을 같이 냅니다. 실제 저장소 접속은 내보낸 파일을 올리는 것으로 대신하고 3주차로 미룹니다.
어긋난 건만 골라 재처리 목록으로 내보내고, 다시 옮긴 뒤의 대조 결과를 이전 회차와 나란히 놓습니다. 대조 300건으로 같은 건이 두 번 세어지지 않는지 확인합니다.
이관 배치가 끝나면 대조가 이어서 돌고, 어긋난 비율이 기준을 넘으면 다음 단계를 진행하지 않고 담당자에게 알립니다.
이관 담당자가 대조를 실행하면 빠진 행과 값이 달라진 행이 사유별로 갈라지고, 비교한 두 값이 같이 보입니다.
업무 키가 비거나 같은 키가 두 번 들어오거나 원본에만 있는 행이 섞인 입력 30종을 넣어도 화면이 죽지 않고, 맞출 수 없는 행은 미매핑으로 남습니다.
대조 300건으로 같은 건이 두 번 세어지지 않는지 확인합니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 이관 정합성은 비바시티의 실 수집 데이터와 이관 배치가 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 업무 키 결측·중복 적재·원본에만 있는 행을 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
값이 다르다는 사실이 곧 이관 오류를 뜻하지는 않습니다. 새 구조에서 일부러 정규화한 값도 다르게 보이므로 판정을 결론으로 읽지 않게 두 값을 항상 같이 보여줍니다.
실제 도입에서는 채널마다 원문 길이 제한과 인코딩이 달라 절단과 깨짐이 생깁니다. 예외가 늘어나면 비교 규칙을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
운영 중인 저장소에 직접 쓰기를 거는 재처리는 권한과 책임이 걸려 있어 이 포트폴리오의 범위 밖입니다. 여기서는 재처리 대상 목록을 내는 데까지만 다룹니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
레거시 저장소에 남아 있는 원본 수집 글입니다. 대조의 한쪽 축이며 업무 키 결측과 같은 글의 재수집 중복을 의도적으로 포함합니다.
| row_id | post_key | channel | collected_at | body_raw | lang_raw | body_len | note |
|---|---|---|---|---|---|---|---|
| src_0001 | steam:88213 | steam | 2026-08-10T03:11:00 | 패치 후 로딩이 두 배로 길어졌어요 | ko | 21 | — |
| src_0002 | discord:44012 | discord | 2026-08-10T04:02:00 | The new event crashes on Android 13 | en | 35 | — |
| src_0003 | reddit:71190 | 2026-08-10T05:40:00 | Matchmaking takes forever after the up… | en | 42 | — | |
| src_0004 | — | xiaohongshu | 2026-08-10T06:15:00 | 更新后角色皮肤显示不出来 | zh | 12 | 원본에 글 식별자가 없음 |
| src_0005 | inven:20551 | inven | 2026-08-10T07:30:00 | 길드전 보상이 안 들어옵니다 | — | 15 | 언어 표기 없음 |
아래 JSON Schema 에 맞는 원본 수집 행을 더 만들어 주세요. {"type":"object","properties":{"row_id":{"type":"string"},"post_key":{"type":["string","null"]},"channel":{"type":"string","enum":["steam","discord","reddit","xiaohongshu","inven"]},"collected_at":{"type":"string"},"body_raw":{"type":"string"},"lang_raw":{"type":["string","null"]},"body_len":{"type":"integer"},"note":{"type":["string","null"]}},"required":["row_id","channel","collected_at","body_raw","body_len"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) post_key 나 lang_raw 가 null 인 행, (2) body_len 이 0 이거나 수백 자에 이르는 극단 행, (3) 같은 post_key 가 두 번 적재된 재수집 중복 행, (4) 채널별로 다른 표기 규칙의 post_key. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"row_id":{"type":"string"},"post_key":{"type":["string","null"]},"channel":{"type":"string","enum":["steam","discord","reddit","xiaohongshu","inven"]},"collected_at":{"type":"string"},"body_raw":{"type":"string"},"lang_raw":{"type":["string","null"]},"body_len":{"type":"integer"},"note":{"type":["string","null"]}},"required":["row_id","channel","collected_at","body_raw","body_len"]}새 구조로 옮긴 결과입니다. 대조의 다른 쪽 축이며 번역 결측과 원문 절단, 원본에 없는 행을 포함합니다.
| mig_id | post_key | channel | body_text | body_len | lang_code | translated_ko | migrated_at |
|---|---|---|---|---|---|---|---|
| mig_0001 | steam:88213 | steam | 패치 후 로딩이 두 배로 길어졌어요 | 21 | ko | 패치 후 로딩이 두 배로 길어졌어요 | 2026-08-16T02:00:00 |
| mig_0002 | discord:44012 | discord | The new event crashes on Android 13 | 35 | en | — | 2026-08-16T02:00:00 |
| mig_0003 | reddit:71190 | Matchmaking takes forever after the up… | 42 | en | 업데이트 후 매칭이 너무 오래 걸립니다 | 2026-08-16T02:00:00 | |
| mig_0004 | inven:20551 | inven | 길드전 보상이 안 들어옵니다 | 15 | — | 길드전 보상이 안 들어옵니다 | 2026-08-16T02:00:00 |
| mig_0005 | steam:88214 | steam | 환불했습니다 | 6 | ko | 환불했습니다 | 2026-08-16T02:00:00 |
아래 JSON Schema 에 맞는 이관 결과 행을 더 만들어 주세요. {"type":"object","properties":{"mig_id":{"type":"string"},"post_key":{"type":["string","null"]},"channel":{"type":"string","enum":["steam","discord","reddit","xiaohongshu","inven"]},"body_text":{"type":"string"},"body_len":{"type":"integer"},"lang_code":{"type":["string","null"]},"translated_ko":{"type":["string","null"]},"migrated_at":{"type":"string"}},"required":["mig_id","channel","body_text","body_len","migrated_at"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) translated_ko 나 lang_code 가 null 인 행, (2) body_len 이 원본보다 짧게 잘린 행, (3) 원본에 없는 post_key 로 적재된 행, (4) post_key 가 null 이라 맞출 수 없는 행. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"mig_id":{"type":"string"},"post_key":{"type":["string","null"]},"channel":{"type":"string","enum":["steam","discord","reddit","xiaohongshu","inven"]},"body_text":{"type":"string"},"body_len":{"type":"integer"},"lang_code":{"type":["string","null"]},"translated_ko":{"type":["string","null"]},"migrated_at":{"type":"string"}},"required":["mig_id","channel","body_text","body_len","migrated_at"]}| TypeScript | 대조 규칙과 화면을 함께 쓰는 언어입니다. 비교 규칙을 한 곳에 두고 러너와 화면이 같은 코드를 씁니다. |
| NestJS | 대조 실행과 결과 조회를 맡는 백엔드입니다. |
| PostgreSQL | 원본과 옮긴 결과의 대조 기록을 담습니다. 업무 키와 대리키를 분리한 스키마가 대조의 정확도를 좌우합니다. |
| Prisma | 대조 결과를 조회할 때 회차·사유별 조건을 붙입니다. |
| BullMQ | 대조를 이관 배치 뒤에 이어 돌리고 재처리 작업을 큐로 넘깁니다. |
| Redis | 회차별 진행 상태와 중복 실행 방지 키를 둡니다. |
| AWS | 3주차부터 배치와 대조 실행을 함께 띄웁니다. |
| LLM | 판정이 끝난 뒤 비교한 두 값을 들어 무엇이 달라졌는지 설명합니다. 어긋남 여부는 항목별 비교 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 어긋난 건을 재처리하고 회차를 비교하는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 이관 배치 뒤에 대조를 이어 돌려 기준 초과 시 멈추는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
diff_code사유 라벨 · post_key + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
identical동일body_truncated본문 절단translation_missing번역 누락missing_in_target옮긴 쪽 누락extra_in_target원본에 없음key_unmapped키 미매핑대조한 한 건의 판정입니다. 사유 코드와 비교한 두 값, 그리고 근거가 된 원본 행과 이관 행의 번호를 함께 남깁니다.
{"type":"object","properties":{"diff_id":{"type":"string"},"post_key":{"type":["string","null"]},"source_row_id":{"type":["string","null"]},"migrated_row_id":{"type":["string","null"]},"diff_code":{"type":"string","enum":["identical","body_truncated","translation_missing","missing_in_target","extra_in_target","key_unmapped"]},"len_gap":{"type":["integer","null"]},"evidence":{"type":"array","items":{"type":"object","properties":{"side":{"type":"string","enum":["source","migrated"]},"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{"type":["string","number","null"]}},"required":["side","field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["diff_id","diff_code","evidence","summary"]}[
{
"diff_id": "df_0001",
"post_key": "steam:88213",
"source_row_id": "src_0001",
"migrated_row_id": "mig_0001",
"diff_code": "identical",
"len_gap": null,
"evidence": [
{
"side": "source",
"field": "body_len",
"agg": null,
"value": 21
},
{
"side": "migrated",
"field": "body_len",
"agg": null,
"value": 21
}
],
"summary": "본문 길이와 언어가 같아 그대로 옮겨졌습니다.",
"hedge": null
},
{
"diff_id": "df_0002",
"post_key": "inven:20552",
"source_row_id": "src_0010",
"migrated_row_id": "mig_0008",
"diff_code": "identical",
"len_gap": null,
"evidence": [
{
"side": "source",
"field": "body_raw",
"agg": null,
"value": "밸런스 패치 좋네요"
},
{
"side": "migrated",
"field": "body_text",
"agg": null,
"value": "밸런스 패치 좋네요"
}
],
"summary": "본문이 글자 그대로 같아 손댈 곳이 없습니다.",
"hedge": null
},
{
"diff_id": "df_0003",
"post_key": "reddit:71191",
"source_row_id": "src_0008",
"migrated_row_id": "mig_0007",
"diff_code": "body_truncated",
"len_gap": 38,
"evidence": [
{
"side": "source",
"field": "body_len",
"agg": null,
"value": 90
},
{
"side": "migrated",
"field": "body_len",
"agg": null,
"value": 52
}
],
"summary": "긴 원문이 52자에서 잘려 38자가 빠졌습니다.",
"hedge": null
},
{
"diff_id": "df_0004",
"post_key": "discord:44012",
"source_row_id": "src_0002",
"migrated_row_id": "mig_0002",
"diff_code": "translation_missing",
"len_gap": null,
"evidence": [
{
"side": "migrated",
"field": "translated_ko",
"agg": null,
"value": null
},
{
"side": "migrated",
"field": "lang_code",
"agg": null,
"value": "en"
}
],
"summary": "외국어 글인데 번역 필드가 비어 화면에서 원문만 보입니다.",
"hedge": null
},
{
"diff_id": "df_0005",
"post_key": "steam:88214",
"source_row_id": "src_0006",
"migrated_row_id": "mig_0005",
"diff_code": "identical",
"len_gap": null,
"evidence": [
{
"side": "source",
"field": "body_len",
"agg": null,
"value": 6
},
{
"side": "migrated",
"field": "body_len",
"agg": null,
"value": 6
}
],
"summary": "짧은 글도 길이와 본문이 같게 옮겨졌습니다.",
"hedge": null
},
{
"diff_id": "df_0006",
"post_key": "steam:88299",
"source_row_id": null,
"migrated_row_id": "mig_0011",
"diff_code": "extra_in_target",
"len_gap": null,
"evidence": [
{
"side": "migrated",
"field": "post_key",
"agg": null,
"value": "steam:88299"
},
{
"side": "source",
"field": "post_key",
"agg": "count",
"value": 0
}
],
"summary": "원본에 없는 글이 옮긴 쪽에만 있어 테스트 적재로 보입니다.",
"hedge": "합성 기준 판정이라 실제로는 원본에서 지워진 글일 수 있습니다."
},
{
"diff_id": "df_0007",
"post_key": null,
"source_row_id": "src_0004",
"migrated_row_id": null,
"diff_code": "key_unmapped",
"len_gap": null,
"evidence": [
{
"side": "source",
"field": "post_key",
"agg": null,
"value": null
},
{
"side": "source",
"field": "channel",
"agg": null,
"value": "xiaohongshu"
}
],
"summary": "원본에 글 식별자가 없어 옮긴 쪽과 맞추지 못했습니다.",
"hedge": null
},
{
"diff_id": "df_0008",
"post_key": "steam:88213",
"source_row_id": "src_0009",
"migrated_row_id": "mig_0001",
"diff_code": "missing_in_target",
"len_gap": null,
"evidence": [
{
"side": "source",
"field": "row_id",
"agg": "count",
"value": 2
},
{
"side": "migrated",
"field": "mig_id",
"agg": "count",
"value": 1
}
],
"summary": "원본에 두 번 적재된 글이 옮긴 쪽에는 한 건만 있어 재수집 중복 처리 기준을 확인해야 합니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. diff_code 가 enum 여섯 값 중 하나인지 확인합니다. 2. diff_id·diff_code·evidence·summary 가 모두 채워졌는지 확인합니다. 3. len_gap 이 evidence 의 원본 길이와 옮긴 길이의 차이와 같은지 다시 계산합니다. 한쪽이 없으면 len_gap 도 null 인지 확인합니다. 4. evidence 의 field 이름이 원본과 이관 결과에 실재하는 컬럼인지 대조합니다. 5. agg 가 없는 evidence 의 value 가 그 행의 값과 같은지 확인합니다. 6. lang_code 가 ko 가 아니면 translated_ko 가 비었을 때 diff_code 는 translation_missing 인지 확인합니다. 이관 결과에는 번역이 있는 행과 없는 행이 모두 있습니다. 7. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 대조 300건을 넣어 같은 업무 키가 두 번 세어지지 않는지, 재처리 뒤 회차 비교에서 해소된 건이 다시 잡히지 않는지 확인합니다. [수동 검증 · 사람이 봅니다] 1. 새 구조에서 일부러 정규화한 값을 어긋남으로 볼지 표본 20건을 눈으로 확인합니다. 2. 원본 중복을 한 건으로 묶을지 두 건으로 셀지는 이관 기준을 따라 담당자가 정합니다.
근거: 수집 실패 감지, 원본 대조, 정합성 검증, 재처리를 나중에 붙이는 기능이 아니라