CarFortune은 현대자동차그룹의 차량용 플랫폼 Pleos Connect에서 동작하는 차량 운세 앱입니다. 운세를 뽑으면 운세 문장과 함께, 그 운세와 연결된 운전 팁을 보여 줍니다. 이 앱에서 코드만큼 손이 많이 간 것이 콘텐츠 400개였습니다.
400개를 혼자 쓰는 대신 초안은 Claude Code로 만들었습니다. 다만 AI가 쓴 문장은 틀려도 그럴듯해 보여서, 읽기만 해서는 문제를 찾기 어렵습니다. 그래서 생성과 검증을 나누고, 검증을 통과한 콘텐츠만 쓰기로 했습니다.
무엇을 만들었나
| 구분 | 개수 | 구성 |
|---|---|---|
| 차량 운세 (좋음) | 100 | 운세 문장 + 축하 연출 + 운전 팁 |
| 차량 운세 (나쁨) | 100 | 운세 문장 + 액땜 연출 + 운전 팁 |
| 동승자 운세 (운전자 +1명 ~ +4명) | 인원별 50 | 운세 문장 + 좌석 + 운전 팁 |
항목 하나는 이런 구조의 JSON입니다. 실제 문장은 앱이 출시되기 전이라 비워 뒀습니다.
{
"id": "vb-001",
"type": "bad",
"theme": "…",
"fortune": "…",
"ritual": { "type": "…", "copy": "…" },
"tip": { "title": "…", "body": "…" }
}1. 쓰기 전에 규칙부터 정했다
차 안에서 보는 콘텐츠라, 재미보다 먼저 지켜야 할 선이 있었습니다. 콘텐츠 정책을 정하고, 데이터 파일의 meta에 함께 적어 뒀습니다.
- 나쁜 운세는 사고, 부상, 불운을 예언하지 않고 주의를 환기하는 톤만 쓴다.
- 좋은 운세는 "운이 좋으니 마음껏" 같은 식으로 안전을 가볍게 여기게 하는 표현을 쓰지 않는다.
- 팁에 들어가는 과태료, 범칙금, 벌점은 2026년 9월 기준으로 확인한 값만 쓰고, 법이 바뀌면 해당 팁을 직접 고친다.
규칙이 파일 안에 있으면 나중에 콘텐츠를 고치는 사람도 같은 기준으로 판단할 수 있습니다.
2. 기계로 잡을 수 있는 문제는 스크립트로
400개를 한 번에 만들면 표현만 살짝 바꾼 비슷한 문장이 반복되기 쉽습니다. 사용자는 며칠만 써도 "어제랑 같은 말"이라고 느낄 겁니다. 그래서 초안을 만들 때 검증 스크립트를 같이 돌렸습니다.
- 테마 태그: 모든 항목에
theme을 달고, 한 구분 안에서는 테마가 겹치지 않게 했습니다. 중복을 사람의 감이 아니라 기계적으로 검사하기 위한 장치입니다. - 완전 중복: id, 운세 문장, 팁 제목, 팁 본문이 똑같은 항목
- 근접 중복: 자카드 유사도가 운세 문장은 0.42, 팁 본문은 0.32 이상인 쌍
- 금지 표현과 스키마: 정책에 어긋나는 표현, 필수 필드 누락
자카드 유사도는 두 집합이 얼마나 겹치는지를 교집합 크기 / 합집합 크기로 나타낸 값입니다. 검사 방식을 이해하기 쉽게 단순화하면 이렇습니다.
function toBigrams(text: string) {
const compact = text.replace(/\s+/g, "");
const grams = new Set<string>();
for (let i = 0; i < compact.length - 1; i += 1) {
grams.add(compact.slice(i, i + 2));
}
return grams;
}
function jaccard(a: Set<string>, b: Set<string>) {
let shared = 0;
for (const gram of a) if (b.has(gram)) shared += 1;
const union = a.size + b.size - shared;
return union === 0 ? 0 : shared / union;
}
function findNearDuplicates(items: Fortune[], threshold: number) {
const grams = items.map((item) => toBigrams(item.fortune));
const pairs: Array<[string, string, number]> = [];
for (let i = 0; i < items.length; i += 1) {
for (let j = i + 1; j < items.length; j += 1) {
const score = jaccard(grams[i], grams[j]);
if (score >= threshold) pairs.push([items[i].id, items[j].id, score]);
}
}
return pairs;
}첫 초안에서 근접 중복 26건이 걸렸고, 전부 다시 썼습니다. 최종본은 오류 0건, 근접 중복 0건으로 통과했습니다.
3. 사실은 공식 자료와 대조했다
더 위험한 건 중복이 아니라 틀린 정보였습니다. 운전 팁에는 과태료나 벌점 같은 숫자가 들어가는데, 이런 숫자는 AI가 그럴듯하게 틀리기 쉬운 영역입니다. 그래서 법규 수치가 들어간 문장은 도로교통공단, 국가법령정보센터, 찾기쉬운 생활법령정보, 국토교통부 지침과 대조했습니다.
초안에서 실제로 찾아 고친 오류는 이런 것들이었습니다.
- 면허 정지와 취소 기준을 혼동: 1년 누산 121점인 면허 취소 기준을 면허 정지 기준처럼 썼습니다. 면허 정지는 벌점 40점부터입니다.
- 과태료에 벌점을 붙임: 무인 단속으로 차량 소유자에게 부과되는 과태료에는 벌점이 없고, 벌점은 현장 단속의 범칙금에 붙습니다. 두 체계를 섞어 쓴 문장이 있었습니다.
- 확인되지 않은 숫자를 단정: 벌점이 빠져 있거나, 출처마다 금액이 다르거나, 도로 종류에 따라 기준이 달라지는 내용을 숫자 하나로 단정한 문장이 있었습니다. 확인된 값으로 바꾸거나 단정하는 표현을 뺐습니다.
- 시행 전 개정안을 사실처럼 씀: 입법예고 단계인 개정 내용을 이미 시행된 것처럼 쓴 문장은 넣지 않고, 시행되면 고쳐야 할 항목의 id를 작업 기록에 남겼습니다.
결과
- 콘텐츠 400개가 중복 검사와 정책 검사를 모두 통과했습니다.
- 금액이 들어간 팁은 34개입니다. 법이 바뀌면 이 팁들을 직접 고쳐야 한다는 점과, 개정안이 시행되면 고칠 항목을 작업 기록에 남겼습니다.
돌아보며
이 작업에서 AI를 쓰는 기준이 분명해졌습니다. AI는 틀린 내용도 자신 있게 씁니다. 그래서 검증 기준을 생성보다 먼저 정해 두었습니다.
검증도 두 종류로 나눴습니다. 중복, 금지 표현, 형식처럼 기계로 판단할 수 있는 건 스크립트에 맡기고, 법규 수치처럼 근거가 필요한 건 공식 자료와 직접 대조했습니다. 400개를 전부 사람이 읽기는 어렵고, 전부 기계에 맡기면 사실 오류를 놓칩니다.
다음에 개선하고 싶은 점도 있습니다.
- 금액의 위치: 금액을 별도 필드로 빼지 않고 팁 본문에 넣었습니다. 문장은 자연스럽지만, 법이 바뀌면 해당 팁을 찾아 직접 고쳐야 합니다.
- 검증 자동화: 지금은 콘텐츠를 만들 때 검증을 돌렸습니다. 콘텐츠를 고칠 때마다 같은 검사가 자동으로 돌도록 빌드 과정에 붙이는 것이 다음 과제입니다.