Skip to content
View CheonKiO's full-sized avatar
  • 성균관대학교

Organizations

@SEI-Team2 @KB-ITL-CodingTest @KB-its-your-life-Final-Project

Block or report CheonKiO

Block user

Prevent this user from interacting with your repositories and sending you notifications. Learn more about blocking users.

You must be logged in to block users.

Maximum 250 characters. Please don’t include any personal information such as legal names or email addresses. Markdown is supported. This note will only be visible to you.
Report abuse

Contact GitHub support about this user’s behavior. Learn more about reporting abuse.

Report abuse
CheonKiO/README.md

안녕하세요. 천기오입니다.

기능이 동작하는 것과 그렇게 만들 이유가 있는 것은 다르다고 생각합니다.
선택지가 여러 개일 때 무엇을 포기하고 무엇을 얻었는지 코드와 문서에 남겨두는 편입니다.

🕒 Timeline

2019.03 – 2025.02🎓 성균관대학교 소프트웨어학과 (전공 4.31 / 4.5 · 우등 졸업)
2024.09 – 2024.12💼 주식회사 위븐 인턴십 — SI 웹개발
2025.03 – 2025.08📚 [KB] IT's Your Life 6기 수료 (우수 훈련생)
2026.01 – 진행중🚀 SSAFY 15기(Java 전공), 1학기 성적 우수 수료

자격 / 인증  정보처리기사 SQLD 삼성전자 SW 역량테스트 B형(Professional) TOPCIT


🛠 Stack

Backend Java Spring Boot MyBatis MySQL Redis

Frontend Vue.js TypeScript

Infra / Tools Docker GitHub Actions Apache Git


🧩 Algorithm

코딩 테스트 대비를 겸해 매일 문제를 푸는 루틴을 유지하고 있습니다. 알고리즘 자체보다 꾸준함을 보여주는 지표라고 생각해서 남겨둡니다.

Solved.ac Profile solved.ac 문제 해결 스트릭 - 최장 88일 연속

[KB] IT's Your Life 코딩테스트 스터디 Repository (2025-03 ~ 2025-06)
SSAFY 코딩테스트 스터디 Repository (2026-02 ~ 2026-04)

🤝 협업 방식

  • 리뷰어가 보기 편하게 남기는 커밋과 PR — 무엇을 왜 바꿨는지 커밋을 세분화하여 분리하여 적고, PR은 작업 내역 / 변경 사항 / 테스트 플랜 형식으로 씁니다.
  • 문서로 합의하고 시작 — 기능 명세서 · 화면 설계서 · ERD를 먼저 확정한 뒤 개발에 들어갑니다.
  • 판단 근거를 코드와 커밋에 남김 — 구조를 바꿀 때는 왜 바꿨는지를 함께 적습니다. (예: bulk insert 도입 커밋에 "1,000개를 하나씩 저장하니 너무 느려서" — 측정치는 주석으로)
  • 흩어진 것을 모으고 비대해진 것을 나누는 역할 — 팀원들이 각자 만든 중복 컴포넌트를 공통화하거나, 커진 화면을 책임 단위로 쪼개는 작업을 세 프로젝트에서 반복해 맡았습니다.
  • 배경이 다른 팀원 고려 — LightHouse에서는 비전공자 팀원 2명이 프로젝트 종료 후에도 각자 환경에서 실행할 수 있도록 OS별 설치 가이드와 빌드 자동화 스크립트를 작성했습니다.


💼 Experience

주식회사 위븐 — 개발팀 인턴 (2024.09 ~ 2024.12)

ICT 학점연계 프로젝트로 참여한 SI 웹개발 인턴십입니다. 실제 클라이언트에게 납품되는 서비스를 다룬 첫 경험이었습니다.

담당 업무

  • 한국MICE협회 웹사이트 개발 및 유지보수
  • Artitutor(발레 온라인 수강 · 콩쿨 신청 플랫폼) 기능 개발
  • AI 기반 웹빌더 Zaemit / Z-Studio QA

사용 기술  jQuery PHP MariaDB Git Docker

여기서 배운 것

"내가 고른 스택이 아닌 환경에서 일하는 법." 학교 프로젝트는 항상 최신 스택을 골라 시작할 수 있었지만, 실무는 이미 돌아가고 있는 코드 위에 얹는 일이었습니다. jQuery와 PHP로 작성된 기존 코드를 읽고 그 컨벤션에 맞춰 작업하면서, 새로 짜는 것보다 남의 코드를 이해하는 데 시간이 더 든다는 걸 체감했습니다.

고객사 요구사항은 PM과 팀장을 거쳐 전달되는 구조였습니다. 직접 고객사와 소통할 기회는 없었지만, 전달된 요구사항에서 애매한 부분을 그대로 두면 나중에 되돌아온다는 걸 배웠습니다. 확실하지 않은 건 구현하기 전에 되묻는 습관이 여기서 생겼습니다.



📌 Projects


🏯 HisTour — AI 기반 문화유산 탐방 기록 서비스 (2026-06-09 ~ 2026-06-25)

3인 팀 · 17일 · SSAFY 관통 프로젝트 담당: 프로젝트 기반 설계, 데이터 배치 적재, AI 해설 파이프라인, 추천·리포트 API, FE 공통 기반 Backend · Frontend

image image image

사진 한 장과 현재 위치로 문화유산을 식별해 AI 해설을 제공하고, 탐방이 끝나면 퀴즈와 리포트로 복습하게 하는 서비스입니다. 국가유산청 데이터 8,314건(미디어 25,066건)을 배치 적재하고 전량 임베딩해 Redis Stack에 인덱싱했습니다.

프로젝트 초기 패키지 구조와 공통 기반(ApiResponse 일관 응답, GlobalExceptionHandler, Redis·Security·Swagger 설정)을 잡았고, 프론트엔드 쪽에서도 디자인 토큰·JWT 인터셉터(401 감지 시 refresh 후 자동 재시도)·라우터 인증 가드 등 공통 레이어를 세팅했습니다.

Java 21 Spring Boot 3.5.1 MyBatis MySQL(SRID 4326) Redis Stack Jedis Vue 3 TypeScript Kakao Maps

🔍 무엇을 고민했나

"두 개의 추천, 다른 기준"

추천 기능이 두 개인데, 둘 다 문화유산 임베딩 유사도를 씁니다. 그런데 거리 조건이 정반대입니다.

여행 중 추천 리포트의 다음 코스 추천
목적 지금 갈 곳 다음에 갈 곳
거리 조건 반경 이내 가까울수록 방문 중심에서 50km 이상 멀어야

처음에는 같은 로직을 재사용하려 했지만, "다음 여행지"로 지금 있는 동네를 추천하는 건 무의미하다는 걸 깨닫고 분리했습니다. 같은 데이터라도 쓰이는 맥락이 다르면 반대 조건이 정답일 수 있다는 걸 배운 사례입니다.

다음 코스 추천은 이렇게 동작합니다.

방문 문화유산 임베딩 평균 → KNN 100개
→ 방문 중심에서 50km 이상 떨어진 후보만 유지
→ 각 후보를 중심으로 반경 25km 클러스터 생성 → 가장 큰 클러스터 선택
→ 유사도 상위 5개 추출
→ Nearest Neighbor 알고리즘으로 이동 경로 정렬

"의미적으로 비슷한 곳"을 찾은 뒤 "실제로 하루에 돌 수 있는 묶음"으로 만드는 게 핵심이었습니다. 클러스터링으로 지역을 좁히고, 경로는 TSP 근사(Nearest Neighbor)로 정렬했습니다. Haversine 거리 계산도 직접 구현했습니다.

🔗 CourseService.java#L21-L22 — 클러스터 반경 25km · 최소 이격 거리 50km

"AI를 쓰면 돈이 나간다"

AI 호출에는 제한이 있었기 때문에, 비용을 최대한 효율적으로 사용할 수 있도록 구조를 짰습니다.

  • 분류와 해설을 분리 — 처음엔 분류와 해설을 하나의 요청에 함께 담았습니다. 문화유산 식별에는 사진이 필요하지만 해설 생성에는 필요 없습니다. 두 호출을 나누어 해설은 문화유산 단위로 공유 캐싱할 수 있게 됐고, 같은 곳을 방문한 두 번째 사용자부터는 AI 호출이 1회로 줄었습니다.
  • 용도별 토큰 상한 차등 — 분류 50 / 요약 500 / 기본 해설 1500 / 심화 해설 2048
  • 모델 역할 분리 — 식별·요약은 GPT-4o, 심화 해설은 Claude Haiku
  • Rate Limiting — Redis INCR + TTL 1분으로 해설 API를 사용자당 분당 3회로 제한

"같은 요청이 동시에 두 번 들어오면?"

캐시가 채워지기 전에 같은 문화유산 요청이 동시에 들어오면 두 요청 모두 AI를 호출했습니다. 락 범위를 문화유산 ID 단위로 좁혀 전체 처리량을 해치지 않으면서 중복 호출만 막았습니다.

🔗 HeritageService.java#L73 — 캐시 확인 → 락 → double-check → AI 호출 → 저장

"제약이 설계를 바꾼다"

  • GMS 게이트웨이의 100KB 요청 제한 → 사진은 512px/JPEG 70%로 압축, 후보 문화유산은 이미지 대신 텍스트로 전달
  • iOS HEIC는 JPEG 변환 시 EXIF가 사라짐 → 변환 원본에서 GPS를 먼저 추출
  • Spring Data Redis가 RediSearch(벡터 검색)를 지원하지 않음 → 해당 부분만 Jedis로 직접 질의
  • 국가유산청 이미지 URL의 CORS 차단 → 백엔드 이미지 프록시 엔드포인트로 우회

💡 얻은 것

  • AI 응답은 신뢰할 수 없는 입력이라는 관점. JSON 형식을 강제하고, 마크다운 코드블록으로 감싸 오는 경우와 토큰 상한으로 잘려 오는 경우까지 검증한 뒤에야 서비스에 반영했습니다. 잘린 응답은 조용히 넘기지 않고 명확한 예외로 전환했습니다.
  • AI 클라이언트를 하나로 통합. 기능별로 각자 AI 클라이언트를 만들면 재시도·파싱·예외 처리가 중복됩니다. 흩어져 있던 클라이언트를 GmsAiClient 하나로 합쳐 AI 연동 지점을 단일화했습니다.
  • 정확한 것과 유용한 것을 구분하는 감각. 지표가 좋아도 사용자 맥락에서 무의미하면 고쳐야 한다는 걸 직접 겪었습니다.
  • 커진 화면은 나눠야 한다는 것. ReportPage 948줄, QuizPage 903줄까지 자란 컴포넌트를 각각 244줄·145줄로 줄이고, 페이지는 상태 관리와 API 호출만 담당하도록 역할을 좁혔습니다.
  • Claude Code로 반복 구현을 빠르게 처리하고, 남은 시간을 성능 개선과 구조 설계에 썼습니다. AI를 코드 생성기가 아니라 판단을 위임하지 않는 협업 도구로 쓰는 법을 익혔습니다.


🏠 LightHouse — 사회초년생을 위한 부동산 안심 진단 서비스 (2025-07 ~ 2025-08)

6인 팀 (전공자 4 · 비전공자 2) · 2개월 · KB IT's Your Life 6기 Final 담당: 실거래가·건축물대장 외부 API 연동, 동네 검색 성능 개선, 공용 컴포넌트, 관심 목록, 백엔드 환경 구성, 설치 가이드 Backend · Frontend

image image image image

전세사기 피해자의 75%가 2030이라는 데이터에서 출발해, 주소만 입력하면 실거래가·건축물대장·재정 안정성을 종합한 안심 리포트를 제공하는 서비스입니다.

Java Spring Framework 5.3.38 MyBatis MySQL Vue 3 TypeScript 공공데이터포털 CODEF Naver Cloud

🔍 무엇을 고민했나

"1,000건을 하나씩 넣으면 얼마나 걸릴까?"

법정동코드 데이터를 적재하는데 체감상 너무 느려서, 원인을 짐작하는 대신 직접 재봤습니다.

방식 1,000건 소요 시간
단건 INSERT 반복 약 2분
bulk INSERT 약 2초

60배 차이였습니다. 이후 나머지 실거래가 API 적재도 전부 bulk insert로 바꿨습니다. 측정한 수치는 코드 주석으로 남겨, 나중에 이 값을 건드릴 사람이 근거를 볼 수 있게 했습니다.

병목을 추측하지 않고 측정한 뒤 고친 첫 경험이었습니다.

"스펙이 제각각인 API 8종을 어떻게 하나로 묶을까?"

아파트 / 연립다세대 / 오피스텔 / 단독다가구 × 매매 / 전월세 — 엔드포인트도 응답 구조도 전부 달랐습니다. 8개를 각각 구현하면 호출·파싱·저장 로직이 8벌로 복제됩니다.

응답 타입만 다르고 흐름은 같다는 점에 착안해, 저장 동작을 SaveHandler<T> 함수형 인터페이스로 추상화하고 제네릭 메서드 하나로 통합했습니다. 각 API는 VO 타입과 매퍼 메서드 레퍼런스만 넘깁니다.

insertCommon(APT_TRADE_ENDPOINT, lawdCd, dealYmd,
    ApartmentTradeVO.class, mapper::insertApartmentTradeBatch, "아파트 매매");

XML 응답은 XmlMapper + constructParametricType으로 제네릭 DTO 하나가 8종을 모두 받도록 했습니다.

🔗 ApiService.java

"거의 같은 두 API를 따로 만들어야 하나?"

CODEF 건축물대장은 일반 건축물집합 건축물로 요청 종류가 나뉘는데, 응답 구조가 거의 같고 집합 쪽에만 동·호 필드가 추가되는 차이였습니다.

두 서비스를 따로 만드는 대신 VO에 타입 구분과 reqDong/reqHo를 추가해 하나로 합쳤습니다. 차이가 필드 두 개뿐인데 클래스를 나누면, 이후 공통 로직을 고칠 때 양쪽을 모두 손대야 한다고 판단했습니다.

"매번 서버에 물어봐야 할까?"

동네(법정동) 검색이 타이핑할 때마다 API를 호출하는 구조라 반응이 느렸습니다. 캐싱이나 디바운스를 붙이는 대신 데이터의 성격을 먼저 봤습니다.

법정동 코드는 바뀌지 않는 정적 데이터였고, 전체가 5,000건 × 6필드 규모였습니다. 초기 로드 비용이 매 검색마다 발생하는 네트워크 왕복보다 싸다고 판단해, 마운트 시점에 전체를 받아오고 검색은 클라이언트에서 처리하도록 바꿨습니다.

데이터가 커지거나 자주 바뀌었다면 반대 선택을 했을 겁니다. 정답이 아니라 이 조건에서의 답이라는 걸 배운 사례입니다.

"확장성과 편의성 중 어디에 맞출까?"

공용 모달 컴포넌트를 만들며 가장 오래 고민한 부분입니다. 모든 경우를 커버하는 하나의 컴포넌트는 props가 비대해지고, 케이스별로 나누면 중복이 생깁니다.

결국 배경 딤 처리(DimOverlay)와 레이아웃(ModalLayout)을 분리하고, 내용이 비어 있는 BaseModal을 새로 두되 기존에 쓰이던 버튼형 모달은 그대로 남겼습니다. 이미 여러 화면이 기존 모달에 의존하고 있었기 때문에, 한 번에 갈아엎는 대신 새 구조를 먼저 안착시키는 쪽을 택했습니다. 이후 모든 화면을 새 모달로 옮기고 나서야 구버전을 삭제했습니다.

"각자 만든 것들이 흩어져 있다"

6명이 각자 화면을 맡다 보니 같은 UI를 여러 사람이 따로 만드는 일이 생겼습니다. 로딩 스피너가 대표적이었는데, 화면마다 조금씩 다른 구현이 흩어져 있었습니다.

이런 것들을 찾아 공통 컴포넌트로 합치고, 공용 코드와 유틸 메서드에는 주석을 달아 다른 팀원이 찾아 쓸 수 있게 정리했습니다. 사용되지 않는 코드도 함께 제거했습니다.

기능을 새로 만드는 일만큼 이미 만들어진 것을 정리하는 일도 필요하다는 걸 배웠습니다. 6명이 2개월간 작업하면 중복은 반드시 생깁니다.

💡 얻은 것

  • "어떻게 최적화할까"보다 "이 데이터가 어떤 성격인가"를 먼저 묻게 됐습니다. 문제 해결이 아니라 문제 정의가 먼저라는 걸 배웠습니다.
  • 반복되는 구조를 추상화할 때 무엇을 공통으로 묶고 무엇을 파라미터로 뺄지 판단하는 기준이 생겼습니다.
  • 기존 코드를 바꿀 때는 마이그레이션 경로까지가 설계라는 것. 새 구조를 만들고 끝내는 것과, 전부 옮긴 뒤 구버전을 지우는 것은 다른 일이었습니다.
  • 배경이 다른 팀원과 일할 때 문서가 곧 협업 도구라는 것.


⚫ Omok — 실시간 온라인 오목 (1차 2025.06 ~ 08 → 2차 2026.05 ~ 06 (재개발))

개인 프로젝트 Backend · Frontend · ▶ Live Demo

image image image

STOMP/SockJS 기반 실시간 대전 오목. Oracle Cloud에 직접 배포해 운영 중입니다.

Java 21 Spring MVC 6 WebSocket/STOMP Vue 3 Pinia Apache Tomcat 10 GitHub Actions

🔍 무엇을 고민했나

"같은 로직을 두 번 짜는 게 낭비일까?"

렌주룰 금수(3-3, 4-4, 장목) 판정을 프론트와 백엔드 양쪽에 구현했습니다. 중복처럼 보이지만 역할이 다릅니다. 프론트는 클릭 즉시 피드백을 주기 위한 것이고, 백엔드는 신뢰 경계입니다. 클라이언트는 조작될 수 있으니 서버가 턴 순서·중복 착수·금수 여부를 전부 재검증한 뒤에만 상태를 반영합니다.

규칙 자체도 단순하지 않았습니다. 흑은 정확히 5목이어야 승리(6목은 장목 금수)이고 백은 5목 이상이면 승리라, 같은 함수로 처리할 수 없었습니다. 또 금수 조건이 성립해도 그 수로 5목이 완성되면 승리가 우선합니다.

🔗 GameService.java — 판정 로직 (버그 수정 근거를 주석으로 남겨둠)

"연결이 끊긴 사람을 바로 패배 처리해야 하나?"

모바일에서는 잠깐 터널에 들어가도 소켓이 끊깁니다. 즉시 퇴장시키면 억울하고, 무한정 기다리면 상대가 묶입니다. 30초 유예를 주고 그 안에 재접속하면 게임을 이어가되, 초과하면 자동 퇴장하도록 했습니다. 반면 나가기 버튼은 명시적 의사표현이라 유예 없이 즉시 처리합니다. 같은 "연결 종료"라도 의도가 다르면 다르게 다뤄야 한다는 판단이었습니다.

🔗 WebSocketEventListener.java#L68-L93 — 게임 중 끊김 vs 게임 외 끊김 분기

"CORS 에러인 줄 알았는데 아니었다"

배포 후 브라우저가 CORS 에러를 뱉었습니다. 실제 원인은 Tomcat이 죽어 있어서 Apache가 503을 반환했고, 그 503 응답에 CORS 헤더가 없어 브라우저가 CORS 문제로 표시한 것이었습니다. WebSocket이 계속 XHR 폴링으로 폴백되던 것도 Apache ProxyPass 경로 순서 문제였고요. 에러 메시지가 원인을 가리킨다고 믿으면 안 된다는 걸 배웠습니다.

💡 얻은 것 — 그리고 두 번 만든 이유

1차는 부트캠프에서 배운 내용을 복습고자 시작한 프로젝트였기에 직접 손으로 짰습니다. 2차는 Claude Code를 적극적으로 활용하며 재개발했습니다. 그 과정에서 1차 때 제가 짠 금수 판정 로직에서 버그 4개를 2차에서 발견했습니다.

  • 보드 바깥 좌표를 빈 칸으로 처리해 열린 3/4를 오판정
  • 대칭 열린 3 패턴(○●C●○)이 양방향에서 이중 카운트되어 3-3 오판정 유발
  • 열린 3 패턴 배열의 길이 불일치
  • 4 패턴 일부 누락

직접 짜봤기 때문에 AI가 만든 코드의 어디를 의심해야 하는지 알 수 있었습니다. AI로 개발하는 속도는 결국 검증할 수 있는 만큼만 유효하다는 게 이 프로젝트에서 얻은 가장 큰 결론입니다.

인프라도 처음부터 끝까지 직접 다뤘습니다. Apache 리버스 프록시·SSL·systemd 서비스 등록·GitHub Actions CI/CD까지, 로컬에서 되는 것과 서버에서 되는 것 사이의 간극을 몸으로 배웠습니다.

🔜 추후 개발 계획

DB 연동(기보 저장·복기) → ELO 레이팅 랭크 → Minimax 기반 AI 대전 순으로 프로젝트를 확장할 생각을 가지고 있습니다.



📫 Contact

각 프로젝트의 상세 아키텍처와 트러블슈팅 기록은 레포지토리 README에 정리되어 있습니다.

GitHub @CheonKiO

Pinned Loading

  1. KB-its-your-life-Final-Project/FinalProjectFront KB-its-your-life-Final-Project/FinalProjectFront Public

    Vue 2

  2. KB-its-your-life-Final-Project/FinalProjectBack KB-its-your-life-Final-Project/FinalProjectBack Public

    Java 2

  3. financial-investment/HisTour_FE financial-investment/HisTour_FE Public

    Vue

  4. financial-investment/HisTour_BE financial-investment/HisTour_BE Public

    Java

  5. omok-front omok-front Public

    Vue

  6. omok-back omok-back Public

    Java