케이제로
경력근거: 다국어, 다통화, 타임존 등 글로벌 서비스에 필요한 구조를 설계하고 구현합니다
※ 실제 산출물이 아닌 AI 예측 시안입니다.
공고는 다국어와 다통화와 타임존을 해외 확장의 첫 문제로 들고, 요구사항을 서버 구조와 데이터 모델로 구체화하는 일을 주요업무에 두며, 되돌리기 어려운 결정을 가려내는 감각을 봅니다. 이 포트폴리오는 그 셋이 겹치는 자리 하나를 골랐습니다. 시각을 어떻게 저장할지가 되돌리기 어려운 결정이라, 지금 데이터가 그 결정을 지키고 있는지 화면으로 확인합니다. 다만 이 포트폴리오는 시간대 변환 판정과 화면 구현을 보여줍니다. 국가별 규제나 현지 결제 정책에 대한 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
다국어와 다통화와 타임존과 국가별 정책처럼 국내에서는 크게 신경 쓰지 않던 문제들이 한꺼번에 생긴다고 공고 첫 단락에 적습니다. 기능을 새로 만드는 일보다 기존 구조를 고치는 판단이 앞섭니다.
서버와 데이터가 중심이지만 필요하면 프론트엔드와 인프라와 모바일까지 다룬다고 적고, 요구사항을 서버 구조와 데이터 모델로 구체화하는 일을 주요업무에 둡니다.
빠르게 결정해도 되는 것과 나중에 되돌리기 어려운 결정을 구분해 개발 속도를 조절해 본 사람을 찾습니다. 데이터 저장 방식처럼 한 번 정하면 바꾸기 어려운 자리를 다루는 요구입니다.
운영 담당자가 기간을 고르면, 저장된 예약 시각을 국가별 시간대로 되돌려 사용자에게 안내된 시각과 어긋난 예약을 나눠 보여줍니다. 어긋났다는 판정은 시간대 변환 규칙이 정하고, 모델은 판정에 손대지 않고 어긋난 사유를 담당자가 읽을 한 줄로 요약합니다.
저는 해외로 나간 레슨 서비스의 운영을 맡고 있습니다. 여름이 되자 유럽 사용자에게서 안내받은 시간과 실제 수업 시간이 다르다는 문의가 옵니다. 예약 데이터를 열어 봐도 저장된 값이 맞는지 화면 표시가 틀린 건지 구분이 안 됩니다.
시간 문의가 반복되는 이유는 개발자가 실수해서가 아니라 저장값과 표시값을 나란히 볼 자리가 없기 때문입니다. 예약마다 저장 시각과 현지 표시를 함께 계산해 어긋난 건만 모아 주면, 고칠 예약과 고칠 코드가 함께 좁혀집니다.
운영 담당자가 쓰는 화면 하나가 완결됩니다. 저장 시각을 국가별 시간대로 되돌려 안내 시각과 비교하고, 어긋난 예약을 사유와 같이 보여줍니다. 실제 서비스 연동은 예약 내보내기 파일을 올리는 것으로 대신하고 3주차로 미룹니다.
수정 대상으로 표시한 예약 160건을 다시 넣어 남은 어긋남을 세고, 사유별로 얼마나 줄었는지 냅니다. 서머타임 경계 같은 어려운 구간이 그 표에서 드러납니다.
예약 생성 시점에 같은 규칙을 돌려 어긋난 값이 저장되기 전에 걸러내고, 국가를 추가하면 그 나라 규칙이 설정으로 붙습니다.
운영 담당자가 기간을 고르면 저장 시각과 현지 표시가 나란히 계산되고, 어긋난 예약이 사유와 같이 보입니다.
시간대 정보가 비거나 자정을 넘는 예약이 섞이거나 등록되지 않은 국가 코드가 들어온 입력 30종을 올려도 화면이 죽지 않고, 판정할 수 없는 예약은 미판정으로 남습니다.
예약 160건을 고친 뒤 다시 넣어 남은 어긋남이 얼마나 줄었는지 냅니다. 단, 정답과 데이터를 모두 작성자가 만든 합성 기준이라 성능 검증이 아니라 데모 조건 동작입니다.
위 수치는 더미데이터 자가채점이며 실제 문의 감소 효과는 케이제로의 실 예약 데이터와 문의 기록이 있어야 측정됩니다. 이 포트폴리오는 그 검증을 바로 돌릴 수 있는 상태로 만듭니다.
수치는 합성 데이터 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 실검증 준비 상태로 솔직히 설명합니다.
정답을 작성자가 만들었으니 잘 맞는 것이 당연합니다. 시간대 결측·서머타임 경계·등록되지 않은 국가 코드를 더미에 일부러 섞어 쉬운 문제만 푸는 함정을 피합니다.
표시가 다르다는 사실이 곧 저장이 틀렸다는 뜻은 아닙니다. 사용자가 여행 중이라 다른 시간대에서 보는 경우도 있으므로 판정을 결론으로 읽지 않게 두 시각을 항상 같이 보여줍니다.
실제 도입에서는 국가마다 서머타임 시행 여부와 전환 날짜가 달라 규칙이 매년 바뀝니다. 국가가 늘어나면 규칙을 코드에서 빼 설정으로 옮겨야 하며, 그것은 도입 단계 과제로 분리합니다.
사용자에게 정정 안내를 보내는 발송은 이 포트폴리오의 범위 밖입니다. 여기서는 고칠 예약 목록을 내는 데까지만 다룹니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
해외 사용자의 수업 예약 기록입니다. 판정의 입력이며 시간대 결측과 서머타임 경계, 등록되지 않은 국가 코드를 의도적으로 포함합니다.
| booking_no | created_at_utc | starts_at_utc | country_code_raw | tz_name | shown_local_time | currency | channel | note |
|---|---|---|---|---|---|---|---|---|
| BK-8001 | 2026-08-10T02:11:00Z | 2026-08-12T18:00:00Z | KR | Asia/Seoul | 2026-08-13 03:00 | KRW | app | — |
| BK-8002 | 2026-08-10T09:40:00Z | 2026-08-13T07:00:00Z | DE | Europe/Berlin | 2026-08-13 08:00 | EUR | web | 서머타임 적용 기간 |
| BK-8003 | 2026-08-11T05:02:00Z | 2026-08-14T12:00:00Z | JP | Asia/Tokyo | 2026-08-14 21:00 | JPY | app | — |
| BK-8004 | 2026-08-11T11:30:00Z | 2026-08-15T22:30:00Z | US | America/New_York | 2026-08-15 17:30 | USD | web | — |
| BK-8005 | 2026-08-12T01:15:00Z | 2026-08-16T06:00:00Z | DE | — | 2026-08-16 07:00 | EUR | web | 시간대 정보 없음 |
아래 JSON Schema 에 맞는 예약 행을 더 만들어 주세요. {"type":"object","properties":{"booking_no":{"type":"string"},"created_at_utc":{"type":"string"},"starts_at_utc":{"type":["string","null"]},"country_code_raw":{"type":"string"},"tz_name":{"type":["string","null"]},"shown_local_time":{"type":["string","null"]},"currency":{"type":["string","null"],"enum":["KRW","USD","EUR","JPY",null]},"channel":{"type":"string","enum":["web","app"]},"note":{"type":["string","null"]}},"required":["booking_no","created_at_utc","country_code_raw","channel"]} 쉬운 데이터만 만들지 말 것 — 다음 하드 케이스를 의도적으로 섞어라: (1) tz_name 이나 starts_at_utc 가 null 인 행, (2) 자정을 넘어 날짜가 바뀌는 예약과 서머타임 전환 당일 예약, (3) country_code_raw 의 대소문자가 흔들린 행, (4) 국가 설정에 없는 country_code_raw. 생성한 행은 반드시 위 스키마를 통과해야 하며, 형식을 깨뜨리는 시도는 별도로 3건만 표시해 두세요.{"type":"object","properties":{"booking_no":{"type":"string"},"created_at_utc":{"type":"string"},"starts_at_utc":{"type":["string","null"]},"country_code_raw":{"type":"string"},"tz_name":{"type":["string","null"]},"shown_local_time":{"type":["string","null"]},"currency":{"type":["string","null"],"enum":["KRW","USD","EUR","JPY",null]},"channel":{"type":"string","enum":["web","app"]},"note":{"type":["string","null"]}},"required":["booking_no","created_at_utc","country_code_raw","channel"]}| TypeScript | 판정 규칙과 화면을 함께 쓰는 언어입니다. 시간대 변환 규칙을 한 곳에 두고 러너와 화면이 같은 코드를 씁니다. |
| PostgreSQL | 예약과 국가 설정을 담습니다. 저장 시각과 시간대 정보를 분리한 스키마가 판정의 기준입니다. |
| AWS | 3주차부터 예약 접수와 검사 실행을 함께 띄웁니다. |
| Cloudflare | 국가별 접속 지점 정보를 참고 값으로 받아 표시 시간대 추정에 씁니다. |
| Inngest | 예약 생성 뒤에 검사를 이어 돌리는 작업 흐름에 씁니다. |
| LLM | 판정이 끝난 뒤 어긋난 사유를 담당자가 읽을 한 줄로 요약합니다. 어긋남은 규칙이 정합니다. |
✓ pass → 그대로 멈춰도 완결입니다. 더 키우려면 고친 뒤 남은 어긋남을 다시 세는 단계로 넘어갑니다.
✓ pass → 여기까지도 완결입니다. 더 키우려면 예약 생성 시점에 같은 규칙을 돌리는 단계로 확장합니다.
위 예측 UI 는 3주차까지 완주했을 때의 화면입니다. 1주차에 실제로 만드는 것은 아래 표시된 화면 하나입니다.
tz_code사유 라벨 · booking_no + 한 줄 요약(근거 수치 포함)화면·코드·면접에서 이 단어를 그대로 씁니다. 키는 출력 스키마의 enum 과 같습니다.
matched일치offset_mismatch시차 불일치dst_boundary서머타임 경계tz_missing시간대 결측start_missing시작 시각 결측country_unmapped국가 미매핑예약 한 건의 시간대 판정입니다. 사유 코드와 비교한 두 시각을 함께 남깁니다.
{"type":"object","properties":{"check_id":{"type":"string"},"booking_no":{"type":"string"},"tz_code":{"type":"string","enum":["matched","offset_mismatch","dst_boundary","tz_missing","start_missing","country_unmapped"]},"offset_gap_min":{"type":["integer","null"]},"evidence":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string"},"agg":{"type":["string","null"]},"value":{"type":["string","number","null"]}},"required":["field","value"]}},"summary":{"type":"string"},"hedge":{"type":["string","null"]}},"required":["check_id","booking_no","tz_code","evidence","summary"]}[
{
"check_id": "tz_0001",
"booking_no": "BK-8001",
"tz_code": "matched",
"offset_gap_min": null,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-08-12T18:00:00Z"
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-13 03:00"
}
],
"summary": "서울 시간대로 되돌린 값이 안내된 시각과 같습니다.",
"hedge": null
},
{
"check_id": "tz_0002",
"booking_no": "BK-8004",
"tz_code": "matched",
"offset_gap_min": null,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-08-15T22:30:00Z"
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-15 17:30"
}
],
"summary": "뉴욕 시간대 변환 결과가 안내 시각과 맞습니다.",
"hedge": null
},
{
"check_id": "tz_0003",
"booking_no": "BK-8002",
"tz_code": "offset_mismatch",
"offset_gap_min": 60,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-08-13T07:00:00Z"
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-13 08:00"
},
{
"field": "tz_name",
"agg": "expected_offset_min",
"value": 120
},
{
"field": "shown_local_time",
"agg": "applied_offset_min",
"value": 60
}
],
"summary": "서머타임 기간인데 표준시로 계산해 안내가 한 시간 이릅니다.",
"hedge": null
},
{
"check_id": "tz_0004",
"booking_no": "BK-8009",
"tz_code": "offset_mismatch",
"offset_gap_min": 60,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-08-20T06:30:00Z"
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-20 07:30"
},
{
"field": "tz_name",
"agg": "expected_offset_min",
"value": 120
},
{
"field": "shown_local_time",
"agg": "applied_offset_min",
"value": 60
}
],
"summary": "같은 독일 예약에서 한 시간 차이가 반복돼 변환 코드를 한 곳에서 고쳐야 합니다.",
"hedge": null
},
{
"check_id": "tz_0005",
"booking_no": "BK-8010",
"tz_code": "dst_boundary",
"offset_gap_min": null,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-10-25T00:30:00Z"
},
{
"field": "note",
"agg": null,
"value": "서머타임 해제 당일"
}
],
"summary": "서머타임이 풀리는 날 새벽이라 같은 현지 시각이 두 번 나타나 사람이 확인해야 합니다.",
"hedge": "합성 기준 판정이라 실제 전환 시각은 나라별 관보를 따라야 합니다."
},
{
"check_id": "tz_0006",
"booking_no": "BK-8005",
"tz_code": "tz_missing",
"offset_gap_min": null,
"evidence": [
{
"field": "tz_name",
"agg": null,
"value": null
},
{
"field": "country_code_raw",
"agg": null,
"value": "DE"
}
],
"summary": "시간대 정보가 없어 국가 기본값으로만 짐작할 수 있어 판정을 미룹니다.",
"hedge": null
},
{
"check_id": "tz_0007",
"booking_no": "BK-8006",
"tz_code": "start_missing",
"offset_gap_min": null,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": null
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-17 09:00"
}
],
"summary": "시작 시각이 저장되지 않아 안내 시각과 맞춰 볼 대상이 없습니다.",
"hedge": null
},
{
"check_id": "tz_0008",
"booking_no": "BK-8008",
"tz_code": "country_unmapped",
"offset_gap_min": null,
"evidence": [
{
"field": "country_code_raw",
"agg": null,
"value": "VN"
},
{
"field": "tz_name",
"agg": null,
"value": null
}
],
"summary": "국가 설정에 없는 코드라 기본 시간대를 정하지 못합니다.",
"hedge": null
},
{
"check_id": "tz_0009",
"booking_no": "BK-8007",
"tz_code": "matched",
"offset_gap_min": null,
"evidence": [
{
"field": "starts_at_utc",
"agg": null,
"value": "2026-08-18T15:00:00Z"
},
{
"field": "shown_local_time",
"agg": null,
"value": "2026-08-19 00:00"
}
],
"summary": "자정을 넘는 예약이지만 날짜까지 맞게 안내됐습니다.",
"hedge": null
}
][자동 검증 · 코드가 판정합니다] 1. tz_code 가 enum 여섯 값 중 하나인지 확인합니다. 2. check_id·booking_no·tz_code·evidence·summary 가 모두 채워졌는지 확인합니다. 3. offset_gap_min 이 -840 이상 840 이하이고, evidence 에 적힌 적용 시차와 기대 시차의 차이와 같은지 다시 계산합니다. 4. evidence 의 field 이름이 예약 데이터에 실재하는 컬럼인지 대조합니다. 5. agg 가 없는 evidence 의 value 가 그 booking_no 행의 값과 같은지 확인합니다. 6. tz_name 이 null 이면 tz_code 는 matched 가 아닌지 확인합니다. 예약에는 시간대가 있는 행과 없는 행이 모두 있습니다. 7. summary 가 존댓말로 끝나는지 확인합니다. [평가 러너 · 2주차] 수정 대상으로 표시한 예약 160건을 다시 넣어 사유별로 남은 어긋남이 얼마나 줄었는지 냅니다. [수동 검증 · 사람이 봅니다] 1. 서머타임 전환 당일 예약을 어떻게 안내할지 표본 20건을 눈으로 확인합니다. 2. 사용자가 여행 중이라 다른 시간대에서 본 경우를 어긋남으로 셀지는 운영 기준을 따라 담당자가 정합니다.
근거: 다국어, 다통화, 타임존 등 글로벌 서비스에 필요한 구조를 설계하고 구현합니다