한 줄 소개
공무원 업무 생산성을 높이고 민원인의 불편을 줄이기 위한 AI 기반 민원처리 시스템. 목록-상세-답변 기본 업무흐름에 담당부서 자동 분류 + 답변불필요 판단 + 유사 민원 검색을 결합해 실제 운영형 UX를 목표로 한다.
1) 프론트엔드 🎨
어필 포인트
- ✅ React + TypeScript + SWC 기반의 빠른 개발/빌드 환경
- ✅ “업무 프로세스 UI”에 최적화한 목록 → 상세 → 답변 동선
- ✅ AI 결과를 UX에 자연스럽게 녹이는 설계
- 담당부서 자동 추천
- 답변 필요 여부 배지/경고
- 유사 민원 안내
핵심 기능 구성
- 민원 목록 페이지
- 민원 상세 페이지
- 민원 답변 페이지
- (Hope 연동) 외부 데이터 기반 목록/상세 표시
중요 코드 1 - 라우팅 뼈대 🧭
// App.tsx (핵심 라우팅 구조 예시)
import { BrowserRouter, Routes, Route } from "react-router-dom";
import ComplaintPage from "./pages/CivilComplaintPage";
import CivilDetailPage from "./pages/CivilDetailPage";
import ReplyPage from "./pages/ReplyPage";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<ComplaintPage />} />
<Route path="/civil/:id" element={<CivilDetailPage />} />
<Route path="/reply/:id" element={<ReplyPage />} />
</Routes>
</BrowserRouter>
);
}
중요 코드 2 - Supabase 상세 조회 🔍
// CivilDetailPage.tsx (요지 예시)
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase";
export default function CivilDetailPage() {
const { id } = useParams();
const [detail, setDetail] = useState<any | null>(null);
useEffect(() => {
if (!id) return;
(async () => {
const { data, error } = await supabase
.from("civil_complaints")
.select("*")
.eq("id", id)
.single();
if (!error) setDetail(data);
})();
}, [id]);
if (!detail) return <div>불러오는 중...</div>;
return (
<div>
<h2>{detail.title}</h2>
<div>작성자: {detail.author_name}</div>
<div>작성일: {detail.created_at}</div>
<pre>{detail.content}</pre>
</div>
);
}
중요 코드 3 - AI 초안 버튼 UX 연결 ✍️🤖
// 예: "AI 초안 생성" 버튼 패턴
async function handleDraft() {
setLoading(true);
try {
const res = await fetch("/api/ai/draft", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title,
content,
category,
}),
});
const json = await res.json();
setDraft(json.draft ?? "");
} finally {
setLoading(false);
}
}
2) 백엔드/플랫폼 🗄️⚙️
어필 포인트
- ✅ Supabase 중심의 빠른 MVP 구축
- Postgres
- REST
- Storage
- Edge Functions
- ✅ 프론트 우선 전략
- UI가 요구하는 데이터를 기준으로 스키마를 정교화
- ✅ 운영형 이슈를 조기에 경험
- created_at 누락
- FK 타입 불일치(uuid vs bigint)
추천 스키마 설계 강조 포인트 📌
- ID 타입은 초기에 통일
- Hope/Civil/Reply 전체를 uuid로 통일하면 확장에 유리
- 작성일 정책은 DB 레벨에서 보장
created_at default now()
중요 코드 4 - created_at 방어적인 테이블 정의 🛡️
-- 예시: 민원 테이블 핵심 컬럼
create table public.civil_complaints (
id uuid primary key default gen_random_uuid(),
seq bigint unique,
title text not null,
content text,
category text,
department_name text,
author_name text,
status text default '접수',
view_count int default 0,
created_at timestamptz not null default now()
);
중요 코드 5 - 답변 테이블 FK 일관성 ✅
create table public.civil_replies (
id uuid primary key default gen_random_uuid(),
complaint_id uuid not null references public.civil_complaints(id) on delete cascade,
author_name text,
content text not null,
created_at timestamptz not null default now()
);
블로그에서 이 부분은 경험담으로 강조하면 좋아. “uuid vs bigint FK 충돌을 겪고 나서, ID는 세계관이라는 걸 배웠다” 같은 톤 😄
3) Edge Functions + AI 연동 🧠🔌
어필 포인트
- ✅ 프론트가 직접 LLM을 물지 않게 Edge Function을 중간 안전 레이어로 둠
- ✅ Secrets로 운영 환경 분리
PY_AI_BASE_URLPY_AI_SHARED_SECRET
- ✅ DEV 직통과 운영 Edge 경로 분리 전략
중요 코드 6 - Edge Function 아키텍처 패턴 🌉
// supabase/functions/hope-ai-draft/index.ts (개념 예시)
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
Deno.serve(async (req) => {
const secret = Deno.env.get("PY_AI_SHARED_SECRET");
const baseUrl = Deno.env.get("PY_AI_BASE_URL");
const auth = req.headers.get("x-shared-secret");
if (!secret || auth !== secret) {
return new Response("Unauthorized", { status: 401 });
}
const body = await req.json();
const aiRes = await fetch(`${baseUrl}/draft`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await aiRes.json();
return Response.json(data);
});
4) AI 모델(담당부서 분류 + 답변불필요) 🤖
어필 포인트
- ✅ 민원 업무에 맞춘 멀티태스크 분류 흐름
- 담당부서 예측
- 답변 필요 여부
- ✅ “라벨이 충분한 부서만 학습” 같은 현실적 데이터 품질 전략
- ✅ 모델 결과를 실제 UX에 끼워 넣는 구조
중요 코드 7 - 추론 결과를 프론트 UX에 녹이는 방식 🧩
// 예: 모델 결과를 화면에 반영하는 규칙 예시
function applyAiSuggestion(result: {
department?: string;
noReplyNeeded?: boolean;
confidence?: number;
}) {
if (result.department && (result.confidence ?? 0) >= 0.6) {
setDepartment(result.department);
}
if (result.noReplyNeeded) {
setFlags((prev) => ({ ...prev, noReply: true }));
}
}
중요 코드 8 - Python 추론 엔드포인트 골격 🧪
# FastAPI (개념 예시)
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class PredictReq(BaseModel):
title: str
content: str
@app.post("/predict")
def predict(req: PredictReq):
# 1) 텍스트 전처리
text = (req.title or "") + "\n" + (req.content or "")
# 2) 모델 추론 (담당부서, 답변불필요)
# department, no_reply, conf = model_infer(text)
return {
"department": "예측부서",
"noReplyNeeded": False,
"confidence": 0.72
}
5) 데이터 수집/동기화(Hope) 🌐
어필 포인트
- ✅ 대규모 실시간화 이전에 AI 적용 우선이라는 전략적 선택
- ✅ 운영형 데이터 전략의 출발점
- seq 기반 중복 방지
- created_at 정확도 확보
- 답변 목록 구조화
중요 코드 9 - Hope 크롤링에서 “답변 목록 JSON” 저장 아이디어 📦
# replies = [{"author":..., "phone":..., "date":..., "content":...}]
replies_json = json.dumps(replies, ensure_ascii=False)
row_out = [
no, title, dept, author, author_phone,
created_at, status, content, views,
replies_json
]
이건 블로그에서 “답변은 단순 텍스트가 아니라 구조화된 업무 이벤트” 라는 메시지로 풀면 정말 멋있게 들어간다.
6) 이 프로젝트에서 특히 강하게 어필되는 “설계 감각” 🎯
1) 프론트 우선 설계
- UI를 먼저 고정하고 DB를 ‘필요 데이터 기반’으로 정교화
2) 이벤트 기반 상태 설계
- 답변 등록이라는 이벤트가 발생하면 상태가 ‘완료’로 바뀌는 구조를 목표
3) AI 우선 가치
- 실시간 추가보다 AI가 업무 시간을 줄여주는 순간을 먼저 만든다
7) 블로그에서 쓰기 좋은 “진짜 이야기 포인트” 5개 ✍️
- 😅 FK 타입 충돌(uuid vs bigint)
- 실무에서 반드시 겪는 성장통
- 🕒 작성일(created_at) 누락
- 운영 안정성의 핵심
- ✅ 답변 이벤트 기반 상태 자동화
- 서비스 신뢰도 직결
- 🤖 AI 멀티태스크 분류
- “업무 보조형 AI”라는 명확한 가치
- 🌉 Edge Function으로 AI 보안 경계 구축
- 단순 기능이 아니라 “운영 설계”를 했다는 증거
8) 추천 블로그 구성(이대로 시리즈 가능) 📚
[1편] 프론트 우선으로 민원 시스템 뼈대 만들기
- 라우팅
- 목록/상세/답변 UX
- 프론트가 요구한 데이터 정의
[2편] Supabase 스키마와 운영 이슈 정리
- uuid 통일
- created_at 정책
- Reply FK 설계
[3편] Edge Functions로 AI 연결하기
- Secrets
- DEV/운영 분리
- 보안 레이어
[4편] 담당부서 자동 분류 + 답변불필요 AI 적용기
- 데이터 필터링 전략
- 모델 평가
- UX 반영
[5편] Hope 크롤링과 증분 동기화(선택)
- 실시간 이전 단계에서의 현실적 설계
오늘의 마무리 🌿
이 프로젝트는 단순히 “민원 게시판을 만드는 일”이 아니라 공공 업무 프로세스를 실제로 줄이는 시스템을 만드는 일이다. 그래서 나는 실시간 수집보다 먼저 AI가 담당부서를 추천하고, 답변 필요 여부를 판단해 업무 시간을 줄이는 순간을 우선 완성하려고 한다. UI, 데이터 정책, AI 모델이 한 몸처럼 움직일 때 이 시스템은 비로소 ‘운영 가능한 서비스’가 된다.
기술 아키텍쳐


기능
" 소개에 앞서 해당 기능의 UI/UX는 제주도민원사이트의 디자인을 유사하게 만든 것입니다. 디자인을 똑같이 만들지 않았으며 해당 기능의 시범 영상을 위해 사용했음을 알려드립니다."
1. 담당 부서 추천
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
- 민원 텍스트 수집: title + content + (카테고리/첨부설명)을 하나의 입력으로 합침
- 전처리: 개인식별정보(전화, 주소 등) 마스킹, 길이 제한(truncate)
- 모델 추론: 한국어 분류 모델로 Top-K 부서 + 확신도(score) 출력
- 업무 UI 표시: “추천 부서 1~5개” + “추천 근거(키워드/유사사례)” 표시
- 피드백 저장: 담당자가 실제 선택한 부서를 기록 → 재학습 데이터로 사용
B. 기술 구현 방법
- 모델 선택: klue/roberta-base 같은 한국어 인코더 + 분류 헤드(Linear)
- 출력: [{department, score}] Top-K
- 서빙: FastAPI /ai/hope/department (예시)
- 입력: { title, content }
- 출력: { topk: [{name, score}], model_version }
C. Supabase 연동
- DB 컬럼 예:
- department_suggested_topk jsonb
- department_selected text (실제 배정)
- department_feedback boolean (추천 채택 여부)
- Edge Function(선택):
- 프론트 → Edge Function → FastAPI (서버 URL 숨김/시크릿 검증)
D. 운영 포인트
- 드리프트 대응: 특정 시기에 민원 패턴이 바뀌면 성능 하락 → 월간 샘플링 평가
- 불확실성 처리: score가 낮으면 “미추천/복수부서”로 표시(강제 배정 금지)
1. 유사민원 추천, AI 초안 작성
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
- 민원 임베딩 생성: 민원 텍스트를 임베딩 벡터로 변환(예: 768-d)
- 벡터 DB 저장: post_id, seq, embedding, title, content, department, created_at 저장
- 검색 시 임베딩 생성: 현재 민원의 임베딩 생성
- 근접 탐색: cosine/L2 유사도로 Top-N 검색
- 결과 표시: 제목/요약/유사도/답변 요약/담당부서 등을 UI에 보여줌
B. 기술 구현 방법(권장 구조)
- 임베딩 모델: (1) 도메인 튜닝된 klue/roberta-base 인코더 또는 (2) 상용 임베딩
- 저장소: Supabase Postgres + pgvector
- 테이블 예:
- hope_post_embeddings(post_id uuid, seq int, embedding vector(768), title text, content text, department text, created_at timestamptz, ...)
C. Supabase SQL(RPC) 적용
- match_hope_posts(query_embedding vector, match_count int, exclude_seq int) 같은 함수로
- embedding <=> query_embedding 거리 기반 정렬
- score = 1 - distance 형태로 반환
- 프론트:
- supabase.rpc("match_hope_posts", { query_embedding, match_count, exclude_seq })
D. 운영 포인트
- 색인/성능: 벡터 인덱스(IVFFlat/HNSW) 적용 + 주기적 리빌드/튜닝
- 데이터 품질: “답변 있는 민원” 우선 노출, 중복 제거(동일 민원 반복)
3. Alan AI (LLM) 답변 생성
동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.
A. 적용 흐름
- 컨텍스트 구성
- 민원 원문(title/content)
- 유사 민원 Top 3~5개(답변 포함)
- 관련 규정/FAQ(있다면 RAG로 추가)
- 프롬프트 정책 적용
- “근거 없으면 모른다고”
- “확정 표현 금지 / 검토 필요 문구 포함”
- “개인정보 재노출 금지”
- LLM 호출
- FastAPI가 LLM(OpenAI/사내/Alan 등) 호출
- 초안 반환
- 초안 텍스트 + 인용 근거(링크/문서ID) + 경고 플래그(민감/불확실)
- 저장 및 검토
- DB에 draft 저장 → 공무원 편집 → 최종 등록
B. 기술 구현 방법(Edge Function 권장 이유)
- 프론트가 LLM 키/AI 서버 URL을 직접 알면 위험
→ 프론트 → Edge Function → FastAPI → LLM 구조가 안전하고 운영이 편함 - Edge Function에서:
- 인증(세션/역할) 체크
- rate limit(남용 방지)
- request size 제한(414/413 방지)
C. FastAPI 설계 포인트
- endpoint 예:
- POST /ai/hope/draft
- 입력 예:
- { seq, title, content, similar_refs:[{seq, answer_summary, answer_content, score}], department_hint }
- 출력 예:
- { draft_text, citations:[...], flags:{sensitive:boolean, low_confidence:boolean}, model_version }
D. 운영 포인트
- 타임아웃: 60초 넘기기 쉬움 → 비동기(큐) 옵션, 또는 스트리밍
- 비용: 길이 제한(요약/상위 k만), 캐시(동일 민원 재생성 방지)
- 감사: 누가 어떤 초안을 언제 생성했는지 로그 남기기
4. 비속어, 스팸 분류
- 입력 시점 필터(민원 작성/접수 시)
- 욕설/차별/성적/위협 키워드 룰 기반 1차 차단(빠르고 명확)
- 모델 기반 분류(운영 고도화)
- 혐오/자해/협박/스팸(광고/도배) 분류 모델로 2차 판별
- 정책 적용
- “차단/보류/경고/담당자 검토” 라우팅
B. 기술 구현 방법
- 룰 기반
- 금칙어 사전 + 정규식(띄어쓰기 변형, 자음 분리 등) 대응
- URL/전화번호 반복, 동일 문장 반복 등 스팸 패턴
- 모델 기반
- 텍스트 분류(멀티라벨 추천):
- 욕설, 혐오, 위협, 성적, 광고, 도배, 개인정보 포함
- 출력: labels + confidence
- 텍스트 분류(멀티라벨 추천):
C. 시스템 적용 지점(가장 중요)
- 프론트에서만 막으면 우회 가능
→ DB insert 직전(Edge Function / 서버)에서 반드시 실행 - 결과 저장:
- moderation_labels jsonb
- moderation_status (allow|review|block)
- moderation_reason text
D. 운영 포인트
- 오탐 관리: 차단 대신 “검토”가 기본(공공 민원 특성상)
- 정책 버전 관리: 금칙어 목록/모델 버전/임계값을 버전으로 기록
#해시태그 #민원처리시스템 #React #TypeScript #SWC #Supabase #EdgeFunctions #FastAPI #AI분류 #담당부서자동추천 #답변불필요 #프로젝트회고 #공공AI
'개발' 카테고리의 다른 글
| 차량 ECU, TCU 맵핑과 AI가 만나다. (0) | 2026.05.15 |
|---|---|
| [ 특별상수상 ]CallPeace (통화 + 평화) | 상담원 헬퍼 웹앱 (7) | 2026.02.02 |
| AutoRepairOps 자동차 정비 예약/운영 플랫폼 (1) | 2025.06.30 |
| [ 자율프로젝트 ] Webtoon 댓글 크롤링 (1) | 2021.10.21 |