| name | job-tracking |
| description | Job application status tracking with SQLite CRUD, Korean NLP query parsing, pipeline analytics, and smart suggestions |
Job Tracking Skill v2.18 (EXP-175: Korean number-word salary ranges + stopWord leak fixes)
Korean Natural Language Query Parsing
์ฌ์ฉ์์ ์์ฐ์ด ์
๋ ฅ์ SQL ์ฟผ๋ฆฌ๋ก ๋ณํ:
| Korean Pattern | SQL Filter | Example Input |
|---|
| ๋ฉด์ /๋ฉด์ ๋ณด๋/๋ฉด์ ์กํ | status = 'interview' | "๋ฉด์ ์กํ ๊ฑฐ ์์ด?" |
| ์ง์ํ/์ง์์๋ฃ/๋๋ | status = 'applied' | "์ง์ํ ๊ฑฐ ๋ค ๋ณด์ฌ์ค" |
| ๊ด์ฌ/๋ถ๋งํฌ/์ฐํด๋ | status = 'interested' | "์ฐํด๋ ๊ณต๊ณ " |
| ํฉ๊ฒฉ/์คํผ | status = 'offer' | "ํฉ๊ฒฉํ ๊ณณ" |
| ํ๋ฝ/๊ฑฐ์ /๋จ์ด์ง | status IN ('rejected','declined') | "ํ๋ฝํ ๊ฑฐ ๋นผ๊ณ " |
| ์ง์์์ /์ง์ํ | status = 'applying' | "์ง์ํ ๊ฑฐ" |
| ๋ถํฉ๊ฒฉ | status = 'declined' | "๋ถํฉ๊ฒฉํ ๊ณณ" |
Company/Keyword Filter
| Korean Pattern | SQL Filter |
|---|
| ์นด์นด์ค/๋ค์ด๋ฒ/ํ ์ค etc. | j.company LIKE '%{keyword}%' |
| ๋ฐฑ์๋/ํ๋ก ํธ/๋ฐ์ดํฐ etc. | j.title LIKE '%{keyword}%' |
| ์ฌํ/์๊ฒฉ/๋ฆฌ๋ชจํธ | j.work_type = 'remote' |
| ํ์ด๋ธ๋ฆฌ๋ | j.work_type = 'hybrid' |
| ์์ธ/ํ๊ต/๊ฐ๋จ etc. | j.location LIKE '%{keyword}%' |
| ์ ์๋์/์ค์ฝ์ด/๋งค์นญ | ORDER BY m.score DESC |
| ์ต์ ์ | ORDER BY a.updated_at DESC |
| ๋ง๊ฐ์/๋ง๊ฐ ๋น ๋ฅธ์ | ORDER BY j.deadline ASC |
| ์ฐ๋ด/๊ธ์ฌ/์ฐ์์
| j.salary IS NOT NULL AND j.salary != '' AND j.salary_min IS NOT NULL |
| ์ฐ๋ด NNNN ์ด์/๋ถํฐ | j.salary_min >= NNNN (๋ง์ ๋จ์) |
| ์ฐ๋ด NNNN~MMMM | (j.salary_min <= MMMM AND j.salary_max >= NNNN) (range overlap) |
| ์ฐ๋ด N์ต ์ด์ | j.salary_min >= N*10000 (์ตโ๋ง์ auto-convert) |
| ๋ง๊ฐ์๋ฐ/๊ณง๋ง๊ฐ | deadline โค 7 days |
| ์ค๋ ๋ง๊ฐ | deadline = today |
| ๋ด์ผ ๋ง๊ฐ | deadline = tomorrow |
| N์ผ ๋จ์ | deadline within N days |
| ๊ธฐํ ์๋/๋ฐ๋๋ผ์ธ ์๋ | j.deadline IS NOT NULL AND j.deadline != '' |
| ๋ง๊ฐ (standalone) | j.deadline IS NOT NULL AND j.deadline != '' |
| ๋ง๊ฐ ์๋ฐ (with space) | deadline โค 7 days |
| ์ ์
| j.experience LIKE '%์ ์
%' OR j.experience LIKE '%๋ฌด๊ด%' |
| N๋
์ด์ | j.experience LIKE '%N%' |
| N๋
์ฐจ | j.experience LIKE '%N%' |
| ๊ฒฝ๋ ฅ (standalone) | j.experience NOT LIKE '%์ ์
%' OR j.experience LIKE '%๋ฌด๊ด%' |
Employment Type Filter (EXP-095)
| Korean Pattern | SQL Filter | Example Input |
|---|
| ์ ๊ท์ง | j.employment_type = 'regular' | "์ ๊ท์ง ๊ณต๊ณ ์์ด?" |
| ๊ณ์ฝ์ง/ํ๊ฒฌ | j.employment_type = 'contract' | "๊ณ์ฝ์ง ๊ด์ฌ ๊ณต๊ณ " |
| ์ธํด | j.employment_type = 'intern' | "์ธํด ๊ณต๊ณ " |
| ํ๋ฆฌ๋์/ํ๋ฆฌ๋์ค | j.employment_type = 'freelance' | "ํ๋ฆฌ๋์ ๊ณต๊ณ ์์ด?" |
Career Stage Filter (EXP-095)
| Korean Pattern | SQL Filter | Example Input |
|---|
| ์๋์ด/senior | j.career_stage = 'senior' | "์๋์ด ํฌ์ง์
์์ด?" |
| ๋ฆฌ๋ | j.career_stage = 'lead' | "๋ฆฌ๋ ํฌ์ง์
์์ธ" |
| ๋ฏธ๋/๋ฏธ๋ค | j.career_stage = 'mid' | "๋ฏธ๋ ๋ ๋ฒจ ๊ด์ฌ ๊ณต๊ณ " |
| ์ฃผ๋์ด/junior | j.career_stage = 'junior' | "์ฃผ๋์ด ๊ณต๊ณ " |
Skill-based Filtering (EXP-078)
| Korean/English Pattern | SQL Filter | Example Input |
|---|
| React / ๋ฆฌ์กํธ | j.skills LIKE '%react%' | "React ๊ณต๊ณ " |
| ํ์ด์ฌ / Python | j.skills LIKE '%python%' | "ํ์ด์ฌ ๊ณต๊ณ " |
| ๋์ปค / Docker | j.skills LIKE '%docker%' | "๋์ปค ์ฐ๋ ๊ณต๊ณ " |
| ์คํ๋ง ๋ถํธ / Spring Boot | j.skills LIKE '%spring boot%' | "์คํ๋ง ๋ถํธ ์ง์ํ ๊ณต๊ณ " |
| k8s / ์ฟ ๋ฒ๋คํฐ์ค / Kubernetes | j.skills LIKE '%kubernetes%' | "k8s ์์ธ ๊ณต๊ณ " |
| ์ฝํ๋ฆฐ / Kotlin | j.skills LIKE '%kotlin%' | "์ฝํ๋ฆฐ ๊ด์ฌ ๊ณต๊ณ " |
| Node.js / ๋
ธ๋ | j.skills LIKE '%node.js%' | "๋
ธ๋ ๊ณต๊ณ " |
| ๋ฆฌ๋
์ค / Linux | j.skills LIKE '%linux%' | "๋ฆฌ๋
์ค ๊ณต๊ณ " |
| ๋ฐ๋ธ์ต์ค / DevOps | j.skills LIKE '%devops%' | "๋ฐ๋ธ์ต์ค ๊ณต๊ณ " |
| ๋จธ์ ๋ฌ๋ / ML | j.skills LIKE '%machine learning%' | "๋จธ์ ๋ฌ๋ ๊ณต๊ณ " |
| ์ ๋ํฐ / Unity | j.skills LIKE '%unity%' | "์ ๋ํฐ ๊ฒ์ ๊ณต๊ณ " |
| ๋น
์ฟผ๋ฆฌ / BigQuery | j.skills LIKE '%bigquery%' | "๋น
์ฟผ๋ฆฌ ๊ณต๊ณ " |
| ๋ญ์ฒด์ธ / LangChain | j.skills LIKE '%langchain%' | "๋ญ์ฒด์ธ ๊ณต๊ณ " |
| ํ์ธํ๋ / Fine-tuning | j.skills LIKE '%fine-tuning%' | "ํ์ธํ๋ ๊ณต๊ณ " |
| ์์ฐ์ด์ฒ๋ฆฌ / NLP | j.skills LIKE '%nlp%' | "์์ฐ์ด์ฒ๋ฆฌ ๊ณต๊ณ " |
88 skills total (see test_korean_nlp_v3.js for full list)
Deadline Urgency Scoring (EXP-035)
Deadlines are computed into urgency levels for prioritization:
| Days Until Deadline | Urgency | Display |
|---|
| < 0 | ๐ด expired | "๋ง๊ฐ๋จ" |
| 0โ3 | ๐ด critical | "์ค๋/๋ด์ผ ๋ง๊ฐ!" |
| 4โ7 | ๐ high | "์ด๋ฒ ์ฃผ ๋ง๊ฐ" |
| 8โ14 | ๐ก medium | "2์ฃผ ์ด๋ด" |
| 15+ | ๐ข low | "์ฌ์ " |
| ์์/์์ | โช none | "์์์ฑ์ฉ" |
SELECT j.title, j.company, j.deadline,
CAST(julianday(j.deadline) - julianday(date('now')) AS INTEGER) as days_left,
m.score
FROM jobs j
LEFT JOIN matches m ON j.id = m.job_id
LEFT JOIN applications a ON j.id = a.job_id
WHERE j.deadline IS NOT NULL AND j.deadline != ''
AND j.deadline NOT LIKE '%์์%'
AND CAST(julianday(j.deadline) - julianday(date('now')) AS INTEGER) > 0
AND a.id IS NULL
ORDER BY julianday(j.deadline) ASC, m.score DESC
LIMIT 20
Deadline formats parsed: YYYY.MM.DD, YYYY-MM-DD, MM/DD, ์์, ์์์ฑ์ฉ
Sorting
| Korean Pattern | SQL ORDER |
|---|
| ์ต์ ์/์ต๊ทผ/์๋ก์ด | ORDER BY a.updated_at DESC |
| ์ ์์/๋งค์นญ์/์ ์๋์ | ORDER BY m.score DESC |
| ํ์ฌ์/์ด๋ฆ์ | ORDER BY j.company |
Composite Query Builder
parse_korean_query(input):
filters = []
order = "a.updated_at DESC"
consumedWords = set()
// Sorting (consume keywords early to avoid keyword spill)
if matches "์ต์ ์" โ order = "a.updated_at DESC", consume
if matches "(์ ์|๋งค์นญ)์" โ order = "m.score DESC", consume
if matches "๋ง๊ฐ์|๋ง๊ฐ ๋น ๋ฅธ์" โ order = "j.deadline ASC", consume
// Status detection
if matches "๋ฉด์ " โ filters.push("a.status = 'interview'")
if matches "์ง์(์๋ฃ|ํ|ํ)" โ filters.push("a.status = 'applied'")
if matches "(๊ด์ฌ|๋ถ๋งํฌ|์ฐ)" โ filters.push("a.status = 'interested'")
if matches "(ํฉ๊ฒฉ|์คํผ)" โ filters.push("a.status = 'offer'")
if matches "(ํ๋ฝ|๊ฑฐ์ |๋จ์ด)" โ filters.push("a.status IN ('rejected','declined')")
if matches "์ง์(์์ |ํ )" โ filters.push("a.status = 'applying'")
// Salary filter (EXP-050)
if matches "(์ฐ๋ด|๊ธ์ฌ|์ฐ์์
)" โ filters.push("j.salary IS NOT NULL AND j.salary != ''")
// Deadline urgency (EXP-050)
if matches "๋ง๊ฐ์๋ฐ|๊ณง๋ง๊ฐ" โ filters.push("deadline within 7 days")
if matches "์ค๋ ๋ง๊ฐ" โ filters.push("deadline = today")
if matches "๋ด์ผ ๋ง๊ฐ" โ filters.push("deadline = tomorrow")
if matches "(\d+)์ผ ๋จ์" โ filters.push("deadline within N days")
if matches "๊ธฐํ ์๋|๋ฐ๋๋ผ์ธ ์๋" โ filters.push("j.deadline IS NOT NULL AND j.deadline != ''")
// Work type
if matches "(์ฌํ|์๊ฒฉ|๋ฆฌ๋ชจํธ)" โ filters.push("j.work_type = 'remote'")
if matches "ํ์ด๋ธ๋ฆฌ๋" โ filters.push("j.work_type = 'hybrid'")
// Skill-based filtering (EXP-078, EXP-079 multi-skill)
// Matches tech skill names (English + Korean aliases) against j.skills column
// Multiple skills are AND-combined: "React TypeScript ๊ณต๊ณ " โ react AND typescript
// Longer skills block substrings: "spring boot" consumed โ "spring" skipped
// Korean aliases: ํ์ด์ฌโpython, ๋์ปคโdocker, ์คํ๋งโspring, ์ฟ ๋ฒ๋คํฐ์คโkubernetes, etc.
// Aliases: k8sโkubernetes, golangโgo, JSโjavascript
// "React ๊ณต๊ณ " โ j.skills LIKE '%react%'
// "ํ์ด์ฌ ์ฅ๊ณ ๊ณต๊ณ " โ j.skills LIKE '%python%' AND j.skills LIKE '%django%'
// "k8s ์์ธ" โ j.skills LIKE '%kubernetes%' AND j.location LIKE '%์์ธ%'
// Negation (๋นผ๊ณ /์ ์ธ/๋ง๊ณ ): negate only the entity immediately before the marker
if matches "(๋นผ๊ณ |์ ์ธ|๋ง๊ณ )" + entity immediately before marker โ that entity gets NOT
if no entity was negated โ fall back to inverting status filter
// Companies (sort by length DESC โ prevents ์นด์นด์ค matching ์นด์นด์ค๋ฑ
ํฌ)
companies.sort((a,b) => b.length - a.length)
for each company:
if consumed by longer company โ skip
apply with negation if immediately before ๋นผ๊ณ /์ ์ธ/๋ง๊ณ
// Location
for each location keyword:
apply with negation if immediately before ๋นผ๊ณ /์ ์ธ/๋ง๊ณ
// Remaining Korean keywords โ BALANCED SQL QUOTES!
for each remaining Korean word (2+ chars) not in stopwords:
filters.push("(j.title LIKE '%{word}%' OR j.company LIKE '%{word}%')")
// Negation fallback
if negation marker present but no entity negated โ invert status filter
return { filters, order }
SQLite CRUD Patterns
Create Application
sqlite3 data/jobs.db "INSERT INTO applications (id, job_id, status, memo, updated_at) VALUES (lower(hex(randomblob(16))), '{job_id}', '{status}', '{memo}', datetime('now'))"
Upsert Application (Create or Update)
sqlite3 data/jobs.db "
INSERT INTO applications (id, job_id, status, memo, updated_at)
VALUES (lower(hex(randomblob(16))), '{job_id}', '{status}', '{memo}', datetime('now'))
ON CONFLICT(job_id) DO UPDATE SET status='{status}', memo=COALESCE(NULLIF('{memo}',''), memo), updated_at=datetime('now')
"
Read Applications (Enhanced with Filters)
sqlite3 -json data/jobs.db "
SELECT a.id, a.status, a.memo, a.updated_at,
j.title, j.company, j.work_type, j.location, j.commute_min, j.source,
m.score
FROM applications a
JOIN jobs j ON a.job_id = j.id
LEFT JOIN matches m ON a.job_id = m.job_id
WHERE {filters}
ORDER BY {order}
LIMIT {limit}
"
Update Application
sqlite3 data/jobs.db "UPDATE applications SET status = '{status}', memo = '{memo}', updated_at = datetime('now') WHERE job_id = '{job_id}'"
Delete Application
sqlite3 data/jobs.db "DELETE FROM applications WHERE id = '{id}'"
Status Transitions
Valid transitions:
interested โ applying โ applied โ interview โ offer
โ rejected
โ rejected
interview โ declined
Any status โ interested (reset)
| Status | Korean | Description |
|---|
| interested | ๊ด์ฌ | Bookmarked, considering |
| applying | ์ง์์์ | Planning to apply |
| applied | ์ง์์๋ฃ | Application submitted |
| rejected | ์๋ฅํ๋ฝ | Resume rejected |
| interview | ๋ฉด์ | Interview stage |
| offer | ํฉ๊ฒฉ | Received offer |
| declined | ๋ถํฉ๊ฒฉ | Not selected after interview |
Pipeline Analytics
Status Distribution
sqlite3 -json data/jobs.db "
SELECT status, COUNT(*) as count
FROM applications
GROUP BY status
ORDER BY CASE status
WHEN 'offer' THEN 1
WHEN 'interview' THEN 2
WHEN 'applied' THEN 3
WHEN 'applying' THEN 4
WHEN 'interested' THEN 5
WHEN 'rejected' THEN 6
WHEN 'declined' THEN 7
END
"
Conversion Funnel
sqlite3 -json data/jobs.db "
SELECT
COUNT(CASE WHEN status IN ('interested','applying','applied','interview','offer') THEN 1 END) as active,
COUNT(CASE WHEN status = 'applied' THEN 1 END) as applied,
COUNT(CASE WHEN status = 'interview' THEN 1 END) as interviewing,
COUNT(CASE WHEN status = 'offer' THEN 1 END) as offers,
COUNT(CASE WHEN status IN ('rejected','declined') THEN 1 END) as rejected,
ROUND(100.0 * COUNT(CASE WHEN status = 'interview' THEN 1 END) / NULLIF(COUNT(CASE WHEN status = 'applied' THEN 1 END), 0), 1) as interview_rate,
ROUND(100.0 * COUNT(CASE WHEN status = 'offer' THEN 1 END) / NULLIF(COUNT(CASE WHEN status = 'interview' THEN 1 END), 0), 1) as offer_rate
FROM applications
"
Top Scored Jobs (Not Yet Applied)
sqlite3 -json data/jobs.db "
SELECT j.title, j.company, j.work_type, j.location, m.score
FROM jobs j
LEFT JOIN matches m ON j.id = m.job_id
LEFT JOIN applications a ON j.id = a.job_id
WHERE a.id IS NULL AND m.score IS NOT NULL
ORDER BY m.score DESC
LIMIT 10
"
Weekly Activity Summary
sqlite3 -json data/jobs.db "
SELECT
date(updated_at, 'weekday 0', '-6 days') as week_start,
COUNT(CASE WHEN status = 'applied' THEN 1 END) as applied_count,
COUNT(CASE WHEN status = 'interview' THEN 1 END) as interview_count,
COUNT(*) as total_updates
FROM applications
WHERE updated_at >= date('now', '-30 days')
GROUP BY week_start
ORDER BY week_start DESC
"
Memo Format Rules
- Plain text, no special characters that break SQL
- Maximum 500 characters
- Escape single quotes: Replace ' with ''
- Include date context when relevant (e.g., "2026-03-30 1์ฐจ ๋ฉด์ ์๋ฃ")
- Format:
{date} {event} {details}
Smart Suggestions
When showing pipeline, optionally suggest:
- Stale applications:
applied status with no update in 14+ days โ "์๋ฅ ๊ฒฐ๊ณผ ํ์ธํด๋ณด์ธ์"
- High-score unapplied: jobs with score > 70 and no application โ "์ด ๊ณต๊ณ ์ ์๊ฐ ๋์์"
- Deadline urgency: unapplied jobs with deadline โค 3 days โ "๐ด {title} ๋ง๊ฐ์ด {N}์ผ ๋จ์์ด์!"
- Interview prep: upcoming interviews โ review job details, company info
- Follow-up needed:
applied > 7 days โ "ํ๋ก์
๋ฉ์ผ์ ๋ณด๋ด๋ณด์ธ์"
- Expired cleanup: jobs past deadline still in
interested/applying โ "๋ง๊ฐ๋ ๊ณต๊ณ ์ ๋ฆฌํ ๊น์?"
Known Companies for NLP Parsing
Common Korean tech companies for query matching:
์นด์นด์ค, ๋ค์ด๋ฒ, ์ผ์ฑ, ๋ผ์ธ, ์ฐ์ํํ์ ๋ค, ํ ์ค, ์ฟ ํก, ๋ฐฐ๋ฌ์๋ฏผ์กฑ, ๋น๊ทผ๋ง์ผ, ์ผ๋์, ํฌ๋ชฝ, ๋ฐฐ๋ฏผ, ๋ฅ์จ, ์์จ์ํํธ, ๋ค์ค์์ฆ, ํ์ปด, ์นด์นด์ค๋ฑ
ํฌ, ํ ์ค๋ฑ
ํฌ, ์๋ฉํ, ๋ง์ดํ๋ ์ด์ค
Location Keywords for NLP Parsing
์์ธ, ๊ฒฝ๊ธฐ, ๋ถ์ฐ, ๋์ , ์ธ์ฒ, ๊ด์ฃผ, ๋๊ตฌ, ์ธ์ฐ, ์์, ์ด์ฒ, ํ๊ต, ๊ฐ๋จ, ์๋ฑํฌ, ์กํ, ์ฑ์, ์ญ์ผ, ์ ์ค, ๋งํฌ, ์ฉ์ฐ, ๊ตฌ๋ก, ๋ถ๋น, ์ผ์ฐ, ํ์ด, ์ธ์ข
, ์ฌ์๋, ์ ์ด, ํ๋, ๊ฑด๋, ๋ํ, ์ฒญ์ฃผ, ์ฒ์, ์์ฌ, ๋
ผํ, ์ ์ฌ, ์ผ์ฑ, ๋ฐฉ๋ฐฐ, ๊ดํ๋ฌธ, ์์ง๋ก, ์ข
๋ก, ์ํฅ, ์์, ์์ฐ, ํํ, ํ์ฃผ, ๊นํฌ, ์ฐฝ์, ํฌํญ
Region Aliases (EXP-159)
| Alias | SQL Translation |
|---|
| ์๋๊ถ | (j.location LIKE '%์์ธ%' OR j.location LIKE '%๊ฒฝ๊ธฐ%' OR j.location LIKE '%์ธ์ฒ%') |
| ์ง๋ฐฉ | (j.location NOT LIKE '%์์ธ%' AND j.location NOT LIKE '%๊ฒฝ๊ธฐ%' AND j.location NOT LIKE '%์ธ์ฒ%') |
| ํด์ธ | (j.title LIKE '%ํด์ธ%' OR j.company LIKE '%ํด์ธ%' OR j.location LIKE '%ํด์ธ%') |
Examples: ์๋๊ถ ๋ฆฌ์กํธ ๊ณต๊ณ , ์ง๋ฐฉ ์ธํด ๊ณต๊ณ , ์๋๊ถ ์ฐ๋ด 6000 ์ด์, ์ง๋ฐฉ ๋ง๊ฐ์๋ฐ ๊ณต๊ณ