서치오에스에이아이
인턴·신입근거: Search OS는 대기업과 대규모 플랫폼을 위한 SEO·AI 검색 자동 최적화 솔루션을 만듭니다
'인프라 변경 없이도 SEO·AI 검색 대응을 자동화할 수 있게 한다' 는 공고 문장이 이 도구의 시작점을 그대로 정의합니다. 고객사 코드에 손대지 않고 이미 갖고 있는 추출 파일만 받아 현장 질문에 답하는 것이 파견 첫 주에 실제로 하는 일입니다. Search OS 가 CSR·SSR·SPA·레거시를 모두 지원한다고 밝힌 만큼 렌더 방식이 질의의 1급 조건 컬럼으로 들어갑니다. 이 포트폴리오가 보여주는 것은 자연어를 검증 가능한 질의로 옮기고 결과를 결정론으로 채점하는 구현이며, 검색 순위 알고리즘이나 GEO 도메인 전문성은 주장하지 않습니다. 인용이 실제로 늘어나는 인과는 모르는 깊이라 면접에서 모른다고 말하는 편이 낫습니다.
공고가 대규모 플랫폼이 인프라 변경 없이 SEO·AI 검색 대응을 자동화하게 한다고 밝힙니다. 고객사 페이지 목록을 파일로 받아 CLI에서 질문에 답하는 이 도구가 인프라를 건드리지 않는 첫 접점입니다.
SSR/CSR/Edge Rendering 구조 이해와 Next.js 경험, SEO/GEO 개념 이해를 요구합니다. 렌더 방식이 질의의 조건 컬럼이 되므로 그 구조 이해가 술어 정의에 그대로 들어갑니다.
분석에서 멈추지 않고 실행까지 자동화하는 데 집착하는 사람을 찾습니다. 고객사 담당자가 물을 때마다 엔지니어를 부르지 않게 질문 자체를 실행 가능한 조건으로 바꾸는 것이 이 도구의 목적입니다.
고객사가 건네준 페이지 인벤토리 파일(사이트맵 크롤 CSV 와 검색 노출 실적 엑셀을 합친 표)을 읽어 두고, 한국어 한 문장을 받으면 그 문장을 필터·집계 조건으로 번역해 실행하는 CLI입니다. 터미널에는 실행된 조건과 사용한 항목, 걸린 페이지 수, 각 URL 이 걸린 이유가 되는 항목 값이 함께 찍힙니다. 기준이 모자란 질문은 되묻고, 인벤토리에 없는 항목을 요구하는 질문은 지어내지 않고 지원 불가로 끝냅니다.
계약한 대형 커머스 고객사에서 페이지 운영을 맡은 담당자입니다. 검색에서 밀리는 페이지를 볼 때마다 추출 파일을 열어 필터를 겹겹이 걸거나 개발팀에 질의를 부탁했고, 답을 받아도 어떤 조건으로 뽑힌 목록인지 몰라 그대로 믿기 어려웠던 사람입니다.
질문을 조건 JSON 으로 바꾸는 단계만 LLM 에 맡기고 실행은 규칙 엔진이 하면, 담당자가 SQL 을 몰라도 스스로 목록을 뽑으면서 조건과 근거 컬럼이 늘 같이 나와 결과를 검증할 수 있습니다.
인벤토리 파일을 읽는 로더와 한국어 질문을 필터·집계 조건으로 번역하는 변환기, 조건을 실행해 조건문·건수·근거 컬럼을 함께 출력하는 CLI 실행기까지 묶은 완결품입니다. 조건 술어 실행기는 단위 테스트로 고정하고, 실크롤 없이 고객사가 준 형태의 추출 파일을 입력으로 시작합니다.
질문과 정답 조건·정답 URL 집합을 짝지은 정답표로 반환 행 집합 완전 일치율을 전수 채점하고, 기준이 모자란 질문은 되묻고 없는 컬럼을 요구하는 질문은 지원 불가로 떨어지는지 회귀 테스트로 굳힌 상태입니다.
자주 묻는 질문을 이름 붙여 저장하고 한 번에 돌려, 조건과 근거 컬럼이 그대로 담긴 마크다운 리포트 파일로 내보내는 흐름이 붙은 상태입니다.
질문 6종을 터미널에 던지면 조건문·사용 컬럼·건수·근거 컬럼 값이 한 화면에 함께 찍히는 흐름을 시연합니다.
질문 정답표 대비 반환 행 집합 완전 일치율을 보여줍니다(표본: 질문 80건, 자가 라벨, 정답 조건이 반환한 URL 집합과 완전 일치할 때만 정답 처리).
기준이 모자란 질문과 인벤토리에 없는 컬럼을 요구하는 질문 25종을 넣어도 조건을 지어내지 않고 되묻기 또는 지원 불가로 떨어짐을 보여줍니다.
위 수치는 작성자가 만든 더미 인벤토리와 자가 작성 정답표 기준이라 성능 증명이 아니라 데모 조건 동작이며, 현장 담당자가 실제로 던지는 질문 분포에서도 버티는지는 서치오에스에이아이 고객사의 실제 파일과 질문 로그가 있어야 측정됩니다. 이 포트폴리오는 그 측정을 바로 돌릴 수 있는 상태로 만듭니다.
일치율은 작성자가 만든 더미 인벤토리와 자가 작성 정답표 기준의 자가채점이라 성능 증명이 아닙니다. 면접에서는 데모 동작과 채점 구조로 솔직하게 설명합니다.
질문도 정답도 내가 만들었으니 잘 맞는 게 당연해지는 함정이 있어, 기준이 모자란 질문과 없는 컬럼을 요구하는 질문을 정답표에 일부러 섞어 되묻기·지원 불가로 떨어지는지를 채점합니다.
이 포트폴리오가 보여주는 것은 자연어를 검증 가능한 조건으로 옮기고 결과를 결정론으로 채점하는 구현이며, 검색 순위와 AI 인용을 실제로 끌어올리는 SEO·GEO 도메인 전문성은 주장하지 않습니다. 모르는 깊이는 면접에서 모른다고 말하는 편이 낫습니다.
실서비스에서는 고객사마다 추출 파일의 컬럼 이름과 결측 방식이 달라 어댑터 손질이 먼저 필요하며, 이는 더미와 달라지는 부분으로 분리해 둡니다.
① 인라인 샘플 · ② seed.json 다운로드 · ③ 증강 프롬프트. 세 층 모두 동일한 schema 에서 파생.
고객사가 사이트맵 크롤과 검색 노출 실적 리포트를 합쳐 건네주는 페이지 목록입니다. 질의가 걸리는 대상이며 결측·깨진 값·정규 주소 충돌을 일부러 담아 조건 분기를 시험합니다.
| url | section | render | title | meta_description | structured_data | canonical | impressions_30d | clicks_30d | last_crawled_at |
|---|---|---|---|---|---|---|---|---|---|
| https://shop.example.com/p/1001 | product | SSR | 겨울 패딩 베스트 10 | Example | 가볍고 따뜻한 겨울 패딩을 가격대별로 비교했습니다. | {"@type":"Product","name":"겨울 패딩"} | https://shop.example.com/p/1001 | 12,400 | 380 | 2026-07-28 |
| https://shop.example.com/p/1002 | product | CSR | — | — | — | — | 90 | 0 | 2026-07-28 |
| https://shop.example.com/p/1003 | product | SSR | 이 페이지는 제목이 너무 길어 검색 결과에서 뒤가 잘려 나가고 무슨 … | 설명 | {invalid json | https://shop.example.com/p/1003 | 2,100 | 12 | 2026-07-28 |
| https://shop.example.com/p/1004 | product | SPA | 제품 상세 | — | https://shop.example.com/p/1099 | 40 | 0 | 2026-07-27 | |
| https://shop.example.com/c/shoes | category | edge | 운동화 전체보기 | Example | 브랜드와 가격대로 운동화를 골라 보세요. | {"@type":"CollectionPage","name":"운동화"} | https://shop.example.com/c/shoes | 30,500 | 1,200 | 2026-07-28 |
다음 JSON Schema 를 그대로 만족하는 페이지 인벤토리 행을 40개 더 만들어 주세요: {"type":"object","properties":{"url":{"type":"string"},"section":{"type":"string","enum":["product","category","blog","event","legacy"]},"render":{"type":"string","enum":["SSR","CSR","SPA","edge","legacy"]},"title":{"type":["string","null"]},"meta_description":{"type":["string","null"]},"structured_data":{"type":["string","null"]},"canonical":{"type":["string","null"]},"impressions_30d":{"type":["integer","null"]},"clicks_30d":{"type":["integer","null"]},"last_crawled_at":{"type":["string","null"]}},"required":["url","section","render"]} url·title·meta 는 실제 커머스·블로그 페이지처럼 현실적이어야 하고 clicks_30d 는 impressions_30d 를 넘지 않아야 합니다. 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) CSR·SPA 라 서버 응답에 제목·메타가 비는 렌더 결측 6건, (2) structured_data 가 깨진 JSON 이거나 name 이 빈 문자열인 행 5건, (3) canonical 이 다른 URL 을 가리키는 정규 주소 충돌 4건, (4) impressions_30d 는 1000 이상인데 clicks_30d 가 0 인 행 4건, (5) 크롤이 실패해 impressions_30d·clicks_30d·last_crawled_at 이 전부 null 인 행 3건, (6) meta_description 이 null 이 아니라 빈 문자열인 행 3건.{"type":"object","properties":{"url":{"type":"string"},"section":{"type":"string","enum":["product","category","blog","event","legacy"]},"render":{"type":"string","enum":["SSR","CSR","SPA","edge","legacy"]},"title":{"type":["string","null"]},"meta_description":{"type":["string","null"]},"structured_data":{"type":["string","null"]},"canonical":{"type":["string","null"]},"impressions_30d":{"type":["integer","null"]},"clicks_30d":{"type":["integer","null"]},"last_crawled_at":{"type":["string","null"]}},"required":["url","section","render"]}질문을 조건으로 옮긴 결과가 맞는지 채점하는 정답표입니다. 질문마다 정답 조건과 그 조건이 반환해야 할 URL 집합을 적어 두어 채점이 규칙으로 끝납니다.
| case_id | question | kind | expected_filter | expected_urls | note |
|---|---|---|---|---|---|
| Q-001 | 서버 응답에 제목이 아예 안 나오는 상품 페이지를 모아 주세요 | filter | section = 'product' AND render IN ('CS… | ["https://shop.example.com/p/1002"] | 렌더 방식과 제목 결측을 함께 봐야 맞는 조건입니다. |
| Q-002 | 노출은 꽤 되는데 클릭이 하나도 없는 페이지를 알려 주세요 | filter | impressions_30d >= 1000 AND clicks_30d… | ["https://shop.example.com/p/1006"] | '꽤'의 기준은 술어 사전에 1000 으로 못 박아 두어 되묻지 않습… |
| Q-003 | 구조화 데이터가 깨졌거나 이름이 비어 있는 페이지를 찾아 주세요 | filter | structured_data IS INVALID_JSON OR str… | ["https://shop.example.com/p/1003","https://shop.example.com/p/1008"] | 값 파싱 실패와 빈 속성을 다른 술어로 나눠야 둘 다 걸립니다. |
| Q-004 | 정규 주소가 자기 자신을 가리키지 않는 페이지를 보여 주세요 | filter | canonical IS NOT NULL AND canonical !=… | ["https://shop.example.com/p/1004","https://shop.example.com/e/summer"] | canonical 이 null 인 행은 충돌이 아니라 결측이라 빠집니다. |
| Q-005 | 메타 설명이 비어 있는 카테고리 페이지가 있나요 | filter | section = 'category' AND (meta_descrip… | ["https://shop.example.com/c/bags"] | null 과 빈 문자열을 함께 묶어야 합니다. |
다음 JSON Schema 를 그대로 만족하는 질문 정답표 행을 30개 더 만들어 주세요: {"type":"object","properties":{"case_id":{"type":"string"},"question":{"type":"string"},"kind":{"type":"string","enum":["filter","aggregate","ambiguous","unsupported"]},"expected_filter":{"type":["string","null"]},"expected_urls":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}},"required":["case_id","question","kind","expected_filter","expected_urls"]} question 은 SQL 을 모르는 운영 담당자가 실제로 쓸 법한 한국어 한 문장이어야 하고, expected_filter 는 page_inventory 의 컬럼만 써야 하며 kind 가 ambiguous·unsupported 이면 null 이어야 합니다. 쉬운 데이터만 만들지 말 것. 다음 하드 케이스를 의도적으로 섞어라: (1) 조건 두 개 이상을 AND·OR 로 엮어야 맞는 질문 8건, (2) 기준값이 빠져 되물어야 하는 ambiguous 5건, (3) 인벤토리에 없는 컬럼을 요구하는 unsupported 4건, (4) 결측(null)과 빈 문자열을 구분해야 정답이 갈리는 질문 4건, (5) 목록이 아니라 집계로 답해야 하는 aggregate 4건.{"type":"object","properties":{"case_id":{"type":"string"},"question":{"type":"string"},"kind":{"type":"string","enum":["filter","aggregate","ambiguous","unsupported"]},"expected_filter":{"type":["string","null"]},"expected_urls":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}},"required":["case_id","question","kind","expected_filter","expected_urls"]}| SSR/CSR/Edge Rendering | 렌더 방식을 질의의 1급 조건 컬럼으로 다룹니다. 서버 응답에 제목이 실리는 경우와 클라이언트에서 채워지는 경우를 술어로 나눠 '서버에서 제목이 안 나오는 페이지' 같은 질문이 조건으로 옮겨집니다 |
| Next.js | 고객사 사이트가 Next.js 인 경우 라우트별 렌더 방식 표기를 인벤토리 컬럼으로 정규화하는 어댑터에 그 구조 이해를 씁니다. 이 도구는 터미널에서 끝나므로 앱 화면은 만들지 않습니다 |
| SEO/GEO | 술어 사전(제목 결측·메타 결측·구조화 데이터 무효·정규 주소 충돌·노출 대비 클릭 없음)을 정의하는 기준입니다. 어떤 컬럼이 검색·AI 인용에서 문제가 되는지가 곧 질의어가 됩니다 |
| TypeScript | 인벤토리 로더와 조건 실행기, CLI 진입점의 구현 언어입니다. 술어별 단위 테스트를 여기서 돌립니다 |
| LLM | 한국어 질문을 조건 JSON 으로 번역하고, 기준이 모자라면 되묻을 문장을 만듭니다. 데이터 판정에는 관여하지 않습니다 |
✓ pass → 여기서 멈춰도 완결입니다. 더 키우려면 정답표 전수 채점과 리포트 내보내기로 넘어갑니다.
✓ pass → 저장한 질문 묶음 실행과 마크다운 리포트 내보내기로 넘어갑니다.
질문 한 건을 실행한 결과입니다. 실행된 조건과 사용한 컬럼, 걸린 페이지와 그 근거 값을 함께 담아 담당자가 목록을 그대로 믿지 않고 검증하게 합니다.
{"type":"object","properties":{"question":{"type":"string"},"status":{"type":"string","enum":["ran","clarify","unsupported"]},"parsed_filter":{"type":["string","null"]},"columns_used":{"type":"array","items":{"type":"string"}},"matched":{"type":"array","items":{"type":"object","properties":{"url":{"type":"string"},"evidence":{"type":"string"}},"required":["url","evidence"]}},"count":{"type":["integer","null"]},"hedge":{"type":"string"}},"required":["question","status","parsed_filter","columns_used","matched","count","hedge"]}[
{
"question": "노출은 꽤 되는데 클릭이 하나도 없는 페이지를 알려 주세요",
"status": "ran",
"parsed_filter": "impressions_30d >= 1000 AND clicks_30d = 0",
"columns_used": [
"impressions_30d",
"clicks_30d"
],
"matched": [
{
"url": "https://shop.example.com/p/1006",
"evidence": "impressions_30d=15200, clicks_30d=0"
}
],
"count": 1,
"hedge": "'꽤 되는'을 술어 사전의 1000 회 기준으로 옮겼습니다. 기준을 바꾸려면 조건을 직접 지정해 다시 실행하시면 됩니다. 합성 인벤토리 기준입니다."
},
{
"question": "성과가 안 좋은 페이지를 정리해 주세요",
"status": "clarify",
"parsed_filter": null,
"columns_used": [],
"matched": [],
"count": null,
"hedge": "성과의 기준 컬럼이 지목되지 않아 실행하지 않았습니다. 노출(impressions_30d)이 낮은 쪽인지 클릭(clicks_30d)이 없는 쪽인지 알려 주시면 조건으로 옮깁니다."
},
{
"question": "경쟁사보다 순위가 밀린 페이지를 알려 주세요",
"status": "unsupported",
"parsed_filter": null,
"columns_used": [],
"matched": [],
"count": null,
"hedge": "이 인벤토리에는 경쟁사 순위 컬럼이 없어 조건을 만들지 않았습니다. 순위 데이터를 컬럼으로 받으면 같은 방식으로 질의할 수 있습니다."
}
][자동 검증 · 스크립트가 전수 수행] 1. 출력이 JSON Schema 형식을 통과하는지 확인합니다(형식이 깨진 결과가 터미널에 나가는 것을 차단). 2. status=ran 인 결과의 matched URL 집합이 정답표의 expected_urls 와 완전히 일치하는지 확인합니다(정답은 LLM 이 아니라 조건 실행 결과가 결정). 3. parsed_filter 에 쓰인 컬럼과 술어가 전부 사전 안에 있는지, columns_used 와 어긋나지 않는지 확인합니다(없는 컬럼을 지어내는 것을 차단). 4. kind 가 ambiguous·unsupported 인 질문에서 matched 가 비어 있고 count 가 null 인지 확인합니다(모르는 질문에 목록이 나가는 것을 차단). [수동 검증 · 사람이 표본 확인] 1. 되묻기 문장이 실제로 필요한 기준을 짚는지 질문 30건을 표본 검토합니다. 2. evidence 에 찍힌 컬럼 값이 인벤토리 원본과 같은지 표본으로 대조합니다.
근거: Search OS는 대기업과 대규모 플랫폼을 위한 SEO·AI 검색 자동 최적화 솔루션을 만듭니다