Dev-Sync는 개발자의 GitHub 활동과 기본 프로필 정보를 기반으로, AI가 이력서/포트폴리오를 자동 생성하고 커뮤니티 피드백까지 연결해 주는 서비스입니다.
- 목표
- 코드와 GitHub 활동만 잘 관리해도 이력서가 자동으로 생성되도록 하는 것
- 생성형 AI를 명확한 도메인 모델과 트랜잭션 안에 안전하게 통합하는 것
- AI 이력서 생성: GitHub 레포지토리/커밋/PR/릴리즈 데이터를 기반으로 OpenAI가 한국어 이력서를 자동 생성
- 블록 기반 이력서 편집기: 프로필/소개/프로젝트/스킬/경력/업적/커스텀 섹션을 블록 단위로 관리 및 정렬
- 커뮤니티 게시판: 카테고리, 검색, 좋아요, 댓글/대댓글이 있는 개발자 커뮤니티
- 문의(컨택트) 시스템: 비밀번호 기반 비공개 문의 + 관리자 이메일 알림
- OAuth 로그인: Google/GitHub OAuth 2.0 기반 세션 로그인, 프로필 관리, GitHub 연동
- 백엔드: NestJS 모노리식 API 서버
- 도메인 모듈:
auth,user,resume,post,contact,upload,queue,worker,common,cache - 데이터: MySQL + TypeORM (RDB 기반 도메인 모델)
- 캐시/큐/세션: Redis + cache-manager + BullMQ
- 도메인 모듈:
- 프론트엔드: React + Vite + TypeScript SPA
- 상태: React Query(서버 상태) + Redux Toolkit(이력서 편집 상태)
- UI: MUI + Emotion, Quill 기반 에디터
- AI & 외부 연동
- GitHub REST API로 활동 데이터 수집
- OpenAI(gpt-4o-mini)로 이력서 JSON 생성 (schema 강제)
- Nodemailer + Gmail로 문의 메일 전송
- 데이터 수집 범위
- 사용자
githubUrl에서 username 추출 /users/:username/repos로 레포 목록 로드- 각 레포마다
/languages로 언어 비율 수집/commits로 최근 커밋 메시지 및 타임라인 수집/contributors로 사용자 기여도 확인/pulls로 사용자 PR 내역 수집/releases로 릴리즈 정보 수집/contents/README.md로 README 텍스트 추출
- 사용자
- 이력서 관점의 스코어링 로직
- 별도의
resume_relevance_score를 계산해 이력서에 넣을 레포를 선별- 스타/포크/워처 수: 외부 관심도 지표
- 기여 횟수 및 PR 수: 본인 기여도 지표
- 릴리즈 수: 배포 경험 지표
- 레포 크기·최근 커밋 여부: 프로젝트의 성숙도와 최신성 반영
- 점수 기반으로 상위 레포만 선별해 AI에 전달
- 별도의
- 입력: GitHub 분석 결과 JSON (상위 레포들의 활동/메트릭/README 요약)
- OpenAI 프롬프트 설계
- 모델에 정확한 JSON 스키마를 제시하고,
response_format: json_object사용 - 제약 조건을 명시:
- 모든 텍스트는 한국어로 작성
- 자기소개/프로젝트 설명/성과는 글자 수 범위를 명시해 과도한 장문 방지
- 데이터에 근거 없는 내용은 빈 문자열/빈 배열로 두도록 규칙화
- 역할/성과는 커밋/PR/릴리즈/README에 근거가 있을 때만 작성
- 프롬프트 본문은 영어로 작성하고, 출력 언어만 한국어로 강제:
- LLM이 이해하기 쉬운 구조적 요구사항과 JSON 스키마를 짧고 일관된 영어 문장으로 표현해 토큰 사용량과 혼동 가능성을 동시에 줄임
- 프롬프트 수정·디버깅 시, 팀원 간 공유와 재현이 용이하도록 한글 설명 대신 영문 스펙 중심으로 관리하는 전략
- 모델에 정확한 JSON 스키마를 제시하고,
- 출력 스키마 예시
introduction: 헤드라인, 자기소개 본문skills: 핵심 스킬, 서브 스킬 목록projects: 프로젝트별 설명/기간/역할/성과 리스트
- 저장 로직
- 결과 JSON을 파싱해 아래와 같이 블록 단위 엔티티로 매핑
- 프로필(ProfileModel)
- 소개(IntroductionModel)
- 스킬(SkillModel + ManyToMany)
- 프로젝트(ProjectModel + ProjectOutcomeModel)
- 스킬 문자열은 DB 내 기존
SkillModel과 이름 매칭 후 ID 기반으로 연결
- 결과 JSON을 파싱해 아래와 같이 블록 단위 엔티티로 매핑
- 블록 타입
PROFILE,INTRODUCTION,PROJECTS,SKILLS,CAREERS,ACHIEVEMENTS,CUSTOM
- 데이터 모델링
- 각 블록은 독립적인 엔티티로 분해 (예:
resume_profile,resume_introduction,resume_project,resume_career등) - 이력서 상세 조회 시, 각 블록을 개별 쿼리로 가져온 후 정렬 정보에 따라 조합하여 응답
- 각 블록은 독립적인 엔티티로 분해 (예:
- 정렬 관리(블록 순서)
OrderModel에(resumeId, blockType, blockId?, order)형태로 순서를 별도 저장- 새 블록 생성 시 가장 뒤에 order 부여, 삭제 시 나머지 order를 재정렬하여 gap 없는 순번 유지
- 프론트 UX 연계
- Redux Toolkit + Immer를 사용해 section 단위 수정/순서 변경을 optimistic update로 처리
- 서버 sync 실패 시 원본으로 롤백하는 트랜잭션 유사 패턴으로 구현
- 게시글 목록 조회
- 카테고리(질문/자유/공지 등), 검색어, 정렬 옵션, 페이지 번호를 입력으로 받습니다.
- TypeORM
QueryBuilder로 작성자/카테고리 조인 및 좋아요/댓글 count를 relation count로 함께 조회
- 게시글 작성/수정
- Quill 에디터를 사용해 이미지/서식이 포함된 글을 작성
- 이미지 업로드는 2단계로 분리:
-
- 프론트: base64 미리보기 + 파일명 관리
-
- 백엔드:
/post/upload에FormData로 전송 → 업로드된 파일 URL 반환 → 에디터 HTML 내src교체 후/post에 최종 HTML 저장
- 백엔드:
-
- 댓글/대댓글 구조
Comment엔티티에서parent필드로 대댓글을 표현- 상위 댓글은 페이지네이션 +
hasReplies플래그 제공 - 대댓글은 별도의 커서 기반 API로 제공해, 긴 쓰레드에도 퍼포먼스를 유지
- 좋아요 토글
(userId, postId)조합에 유니크 제약을 걸어 중복 좋아요를 DB 레벨에서 방지- 좋아요/취소를 단일 토글 API로 제공해 프론트 구현을 단순화
- 문의 등록
- 사용자가 이름/이메일/제목/내용을 제출하면
- DB에 저장 후,
- Nodemailer + Gmail SMTP로 관리자 메일 발송
- 사용자가 이름/이메일/제목/내용을 제출하면
- 비공개 문의
- 4자리 비밀번호를 설정하면, 비밀번호 검증이 통과할 때만 상세 조회/수정/삭제가 가능
- 문의 목록 조회
- 이메일 + 페이지네이션 기반으로 "내가 남긴 문의" 목록을 조회
- 비공개 문의는 메타 정보만 노출하고, 실제 본문은 보호
-
NestJS
- 선택 이유
- 인증, 캐시, 큐, 설정, 유효성 검증 등 서버에서 반복되는 concern들을 일관된 모듈/데코레이터 기반 패턴으로 제공
- 도메인 모듈별로 코드를 나누기 쉬워, 기능 확장 시 코드 베이스가 무너지지 않도록 유지
- 대안 대비
- Express/Fastify를 직접 사용하는 경우, 아키텍처를 개발자가 일일이 설계해야 하는데, 본 프로젝트 규모에서는 Nest의 규칙적인 구조가 더 적합하다고 판단
- 선택 이유
-
TypeORM + MySQL
- 선택 이유
- 이력서/프로젝트/성과/스킬/커뮤니티(게시글/댓글/좋아요) 등 관계형 데이터가 많아 정규화된 RDB 모델이 자연스러움
- QueryBuilder를 통해 복잡한 조인과 페이지네이션, 카운트 쿼리를 직관적으로 작성 가능
- 대안 대비
- Prisma 역시 고려했으나, 복잡한 서브쿼리/집계에 있어서 QueryBuilder 기반 ORM이 더 유연하다고 판단
- 선택 이유
-
Redis + cache-manager-redis-yet
- 선택 이유
- 빈번히 조회되는 유저/카테고리 정보는 Redis에 캐싱해 DB 부하를 낮추고 응답 시간을 단축
CacheService로 네임스페이스(user:email,category:categories)를 추상화해 캐시 사용 코드를 정돈
- 선택 이유
-
BullMQ + Worker 프로세스
- 선택 이유
- OpenAI API 호출과 이력서 저장은 수 초 이상 걸릴 수 있는 작업이므로, HTTP 요청/응답 흐름에서 분리
- BullMQ를 통해 재시도, 작업 상태 모니터링, 백오프 정책 등 백그라운드 작업에 필요한 기능을 손쉽게 사용
- 선택 이유
-
Passport (Google/GitHub OAuth) + 세션 기반 인증
- 선택 이유
- 브라우저 기반 서비스에서 세션 쿠키는 보안과 편의성의 균형이 좋은 선택지
- Access Token은 서버에만 보관하고, GitHub API 호출 시에만 사용해 토큰 노출 위험을 줄임
- 선택 이유
-
React + Vite + TypeScript
- 선택 이유
- Vite는 빠른 HMR과 가벼운 번들 환경을 제공하여 개발 속도와 DX가 우수
- TypeScript를 통해 도메인 모델(이력서 섹션/게시글/댓글 등)의 타입을 명확히 모델링
- 선택 이유
-
React Query
- 선택 이유
- 게시글/댓글/좋아요/이력서 목록/유저 정보 등은 모두 서버 상태이므로, 캐싱/리페치/에러/로딩 상태를 컴포넌트에서 분리하고 싶었음
- 쿼리 키(
userKeys,postKeys,resumeKeys)를 중앙에서 관리해, API 계층을 정돈
- 선택 이유
-
Redux Toolkit + redux-persist
- 선택 이유
- 이력서 편집 상태는 서버와 1:1로 즉시 동기화되지 않는 작업 중 상태(draft) 가 존재
- 섹션 추가/삭제/정렬/부분 업데이트를 Immer 기반으로 쉽게 다루기 위해 Redux Toolkit 사용
- 이력서 편집 도중 새로고침해도 상태가 유지되도록,
redux-persist로 로컬 스토리지에 저장
- 선택 이유
-
MUI + Emotion
- 선택 이유
- 빠르게 일관성 있는 UI를 제공하면서도, Emotion으로 세밀한 스타일 커스터마이징이 필요했음
- 이력서/커뮤니티/문의 등 여러 화면의 공통 컴포넌트를 재사용하기 좋음
- 선택 이유
-
Quill 에디터
- 선택 이유
- 이미지/리스트/강조 등 블로그 수준의 서식을 지원해야 했고, HTML 저장이 가능한 검증된 에디터가 필요했음
- 이미지 리사이즈 모듈과 조합하여 모바일에서도 읽기 좋은 게시글 화면을 구현
- 선택 이유
-
환경 변수 검증
ConfigModule + Joi로 env 스키마를 정의해, 필수 값이 없거나 잘못된 경우 애플리케이션이 부팅 단계에서 바로 실패하도록 설계
-
민감 정보 보호
ClassSerializerInterceptor와@Exclude()를 사용해githubAccessToken등 민감 필드를 API 응답에서 숨김- OAuth Access Token은 DB에 AES로 암호화해 저장, 서버 내부에서만 복호화
-
확장성
- 세션/캐시/큐 모두 Redis를 사용해, 멀티 인스턴스 환경에서도 세션 일관성을 유지할 수 있도록 설계
- 모듈화된 도메인 구조 덕분에, 기능 추가 시 영향을 받는 영역을 명확히 파악 가능
-
에러 처리 및 롤백
- 이력서 블록 sync/업데이트는
typeorm-transactional을 도입해 트랜잭션 단위로 처리 - 프론트에서는 optimistic update 후 실패 시 원본으로 롤백하는 패턴을 적용해 사용자 경험과 데이터 정합성을 함께 확보
- 이력서 블록 sync/업데이트는
-
이력서 공유 링크 & 공개 프로필 페이지
- 현재는 편집/관리 중심이라, 공개용 이력서 뷰와 링크 공유 기능을 추가하면 채용/포트폴리오 활용성이 높아질 예정입니다.
-
GitHub Actions 기반 포트폴리오 자동 갱신
- main 브랜치에 변경이 머지될 때마다 GitHub Actions로 테스트/빌드를 수행하고, 성공 시 정적 포트폴리오 페이지(README, 관련 문서)를 자동으로 빌드·배포하는 파이프라인을 추가해, 코드 변경과 포트폴리오 상태가 항상 동기화되도록 할 계획입니다.
-
테스트 커버리지 보강
- 도메인 서비스 레벨의 유닛 테스트와, Resume 생성 플로우에 대한 E2E 테스트를 추가해 회귀를 막을 계획입니다.