PYAN
  • Home
  • Skills
  • Projects
  • Experience
  • Achievements
  • Education
  • Summary
  • Contact

이상헌

Full Stack Developer & Game Programmer

Experience

개발연구원
·(주) 웰비아닷컴·2021.12 – 2023.02

게임의 안티치트 모듈 및 프로그램인 Xigncode3를 개발하는 회사입니다. · C++로 개발된 보안 모듈을 Unity, 안드로이드 플랫폼에 적용 · JNI 프레임워크를 활용한 C++, Java 간 크로스플랫폼 연동 구현 · 언리얼 엔진(UE4) 소스 코드 분석을 통한 글로벌 오프셋 취약점 발견 및 대응방안 제시 · ACTk Obscured Variable 분석 및 메모리 변조 방지 메커니즘 연구 · Windows Powershell을 이용한 개발환경 자동화 스크립트 구축

부회장 및 리버싱 멘토
·국립금오공과대학교 정보 보안 동아리 BOSS·2023.03 – 2024.08

회사를 다니며 습득한 게임 보안 지식을 바탕으로 후배들에게 멘토링을 제공했습니다. · 리버싱 교육 커리큘럼 설계 및 실습 자료 제작 · 디버깅 툴(IDA)을 활용한 정적 분석 멘토링 · 메모리 변조 툴(Cheat Engine 등)을 이용한 동적 분석 멘토링 · 비전공자도 이해할 수 있는 보안 기초 개념 교육 진행 · 내부 CTF 개최 및 동적 분석 관련 문제 제작

학부연구생
·신호처리 및 지능형네트워크 연구실·2023.07 – 2025.02

학부연구생으로서 연구실에서 진행한 다양한 프로젝트에 참여하였습니다. · 프로젝트 내 프론트엔드 개발 · 프로젝트 내 UI/UX 디자인 · 마크다운 에디터 및 수식 에디터 결합 연구


Projects

엔버봇
2022.04 - 현재 서비스 중🔗

+1

Discord 내에서 메이플스토리에 관련된 정보를 사용자에게 제공하는 챗봇입니다. Nexon Open API를 활용하여 실시간 게임 정보, 캐릭터 검색, 길드 정보 등을 제공하고 있습니다. 약 3년 간 서비스 중이며, 현재 약 4,700개의 서버에서 누적 18만 명 이상이 이용하고 있습니다.

팀 구성

1인 개발

담당 역할

챗봇 및 웹 사이트 개발 운영 및 유지보수 일체

Java
JDA
Maven
Nexon Open API
jsoup
REST API
Gson
Linux
JavaScript
FastAPI
React
MUI
axios
도전과제
문제대규모 동시 요청 처리로 인한 응답 지연
해결샤딩을 통한 서버별 스레드 분산, 비동기적 처리 과정을 도입해 각 명령어에 대한 수행 과정이 따로 진행될 수 있도록 처리함. 이 방안을 통해 응답 시간을 1~2초 가량으로 단축시킬 수 있었음.
결과메이플스토리 파트너 선정 및 게임 유저수 급증으로 인한 봇 이용자 급증. 이에 따라 5초 이상의 응답 지연이 생겨 Discord 정책인 3초 이내 응답 요건 충족 실패. 사용자가 명령어를 입력했지만 응답을 받지 못하는 일이 발생함.
문제제한된 서버 리소스에서의 안정성 확보
해결재사용이 가능한 리소스는 캐시로 저장하도록 변경. 크론탭 기반 실시간 메모리 모니터링 시스템 및 자동 재가동 시스템을 구축하여 해결함. 이 방안을 통해 가동률 98% 이상을 달성할 수 있었음. 추가로, 현재는 서버 업그레이드를 진행하여 데이터 마이그레이션을 완료함.
결과1코어 CPU, 1GB 메모리 환경의 서버에서 OOM 에러가 빈발함. 이에 봇 서버가 종료되어 가동률이 낮아지는 문제가 발생함.
문제직관적이지 않은 명령어 입력 방식 개선
해결사용자가 놓치는 부분이 없도록 데이터 입력부 미작성에 대한 피드백을 강화함. 또한 수많은 예외 처리 분기점을 만들어 최대한 많은 상황에서의 에러를 처리할 수 있도록 하여 해결함. 이 방안을 통해 관련 문의사항이 기존 대비 80% 이상 줄어들었음.
결과사용자들이 명령어 사용법을 어려워 함. 이에 따라 관련 문의가 자주 들어오는 문제가 발생함.
문제제한된 UI 내에서의 사용자 경험의 향상
해결최대한 많은 시행착오를 겪어가며 직접 QA 테스트를 진행하고, 어떤 방식으로 데이터를 표현하는 것이 가장 직관적인지 연구함. 표현 방식의 케이스를 3가지 정도 선별해 실 사용자들에게 의견을 구함. 이 방안을 통해 필수적인 정보는 다 담으면서도 UX를 저하시키지 않을 수 있었음.
결과Discord 메시지로만 정보를 표현할 수 있다는 조건 때문에, 너무 많은 정보는 사용자 경험을 해치는 문제가 되었음.
코드 보기 (5)
공지사항 알리미 코드 일부(java)샤딩을 통해 대규모 서버 분산 처리를 진행했습니다.
// 스레드풀 크기별 채널 분할하여 병렬 처리
int threadPoolSize = MESSAGE_THREAD_POOL_SIZE;
int channelsPerThread = (int) Math.ceil((double) activeChannelInfos.size() / threadPoolSize);

// 각 채널별 완료 추적을 위한 카운터
CountDownLatch messageCompletionLatch = new CountDownLatch(activeChannelInfos.size());

// 채널을 스레드별로 분할하여 병렬 처리
for (int i = 0; i < activeChannelInfos.size(); i += channelsPerThread) {
    final List<ChannelInfo> channelShard = activeChannelInfos.subList(startIndex, endIndex);
    
    messageExecutor.submit(() -> {
        // 이 샤드의 채널들에 순차 전송
        for (ChannelInfo channelInfo : channelShard) {
            channel.sendMessageEmbeds(embed).queue(
                success -> messageCompletionLatch.countDown(),
                failure -> {
                    String failureReason = getFailureReason(failure);
                    // 에러 타입별 채널 상태 관리
                    messageCompletionLatch.countDown();
                }
            );
        }
    });
}
캐릭터 정보 명령어 코드 일부(java)Nexon Open API 연동 및 데이터를 파싱하는 코드입니다.
// GetCharacterBasicAPI.java - API 호출 및 JSON 파싱
public static String[] getCharacterInfo(String name) {
    // OCID 획득
    String ocid = GetOcidAPI.getOcid(name);
    if(ocid.equals("error")) return null;
    
    // API 호출 및 데이터 파싱
    String url = NexonOpenAPIConfig.base_url + "character/basic?ocid=" + ocid;
    String jsonStr = fetchDataFromUrl(url);
    
    if(jsonStr.equals("error")) {
        return new String[] { "API is in Error" };
    }
    
    Gson gson = new Gson();
    Map<String, Object> map = gson.fromJson(jsonStr, Map.class);
    
    // 데이터 가공 및 반환
    String worldImageUrl = worldImageUrlTable(map.get("world_name").toString());
    String level = "Lv." + (int)(Double.parseDouble(map.get("character_level").toString())) 
                  + " (" + map.get("character_exp_rate").toString() + "%)";
    String imageUrl = map.get("character_image").toString().replaceAll("https://", "http://");
    
    return new String[] {worldImageUrl, level, job, popular, guild, updated, userName, imageUrl, ocid};
}

private static String fetchDataFromUrl(String url) {
    try {
        Document doc = Jsoup.connect(url)
                .header("x-nxopen-api-key", NexonOpenAPIConfig.API_KEY)
                .ignoreContentType(true).get();
        
        return doc.select("body").text();
    } catch (Exception e) {
        System.out.println("ERROR :: " + url);
        e.printStackTrace();
        return "error";
    }
}
경험치 히스토리 명령어 코드 일부(java)경험치 상승량 제공을 위한 경험치 계산 로직입니다.
// BigDecimal을 사용한 정밀 경험치 계산
private static String upExpCalc(int sLevel, int eLevel, double sPercent, double ePercent) {
    BigDecimal exp = new BigDecimal(0);
    
    for(int i = sLevel; i <= eLevel; i++) {
        if(i == sLevel) {
            BigDecimal tmp = ExpTables.calcExpPercentToDecimal(sLevel, 
                          (Math.round((100.0 - sPercent) * 1000) / 1000.0));
            exp = exp.add(tmp);
        } else if(i == eLevel) {
            BigDecimal tmp = ExpTables.calcExpPercentToDecimal(eLevel, 
                          (Math.round(ePercent * 1000) / 1000.0));
            exp = exp.add(tmp);
        } else {
            exp = exp.add(ExpTables.expTable()[i - 200]);
        }
    }
    
    return exp.setScale(0, RoundingMode.HALF_UP).toString();
}
스타포스 명령어 코드 일부(java)명령어 수신 후 비동기 작업 처리 및 사용자 피드백을 진행하는 코드입니다.
// 슬래시 커맨드 처리 - 스타포스 시뮬레이터
if(e.getName().equals("스타포스")) {
    e.deferReply().queue();
    
    // 파라미터 검증
    if(option1 == null || option2 == null || /* ... 기타 옵션들 */) {
        e.getHook().sendMessageEmbeds(mapleUserTypingFail()).queue();
        return;
    }
    
    // 즉시 대기 메시지 전송 후 백그라운드 처리
    e.getHook().sendMessageEmbeds(mapleStarForceSimulatorWaiting()).queue(hook -> {
        mapleStarForceSimulatorThreading(level, start, end, starCatch, retry, 
                                       preventDestroy, mvpGrade, isPCRoom, eventType, e, hook);
        System.out.println("Waiting Message Send Success :: " + Main.getNowTimeOnly());
    });
}

// 비동기 시뮬레이션 처리
void mapleStarForceSimulatorThreading(@params) {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            try {
                e.getHook().editMessageEmbedsById(hook.getId(), 
                    mapleNewStarForceSimulator(level, start, end, starCatch, retry, 
                                             preventDestroy, eventType, mvpGrade, isPCRoom)).queue();
            } catch (Exception ex) {
                e.getHook().editMessageEmbedsById(hook.getId(), 
                    mapleStarForceSimulatorFailed()).queue();
                ex.printStackTrace();
            }
        }
    }, 0);
}
스타포스 명령어 임베드 생성 명령어 일부(java)시뮬레이션 이후 유저에게 보내는 메시지를 생성하는 코드 일부입니다.
// 스타포스 시뮬레이션 결과 임베드 생성
MessageEmbed mapleNewStarForceSimulator(@params) {
    MapleNewStarForceSimulator mscs = new MapleNewStarForceSimulator(level, start, end, 
                                                                   starCatch, retry, preventDestroy, eventType, mvpGrade, isPCRoom);
    maple.MapleNewStarForceSimulator.ReturnNode n = mscs.calculate();
    
    if (n.getCount() == -1) {
        return mapleStarForceSimulatorErrorMessage();
    }
    
    EmbedBuilder eb = new EmbedBuilder();
    eb.setAuthor("스타포스 시뮬레이터");
    eb.setColor(new Color(255, 192, 203));
    
    // 이벤트별 상세 정보 처리
    String eventTypeText = getEventTypeText(eventType);
    String mvpGradeText = getMvpGradeText(mvpGrade);
    
    if ((n.getDestroyed() == 1) && !retry) {
        eb.setDescription("아쉽지만 아이템이 파괴되었어.");
        eb.addField("파괴 전 마지막 스타포스", n.getEnd() + " 성", false);
    } else {
        eb.setDescription("강화에 성공했어!");
        eb.addField("결과", n.getEnd() + " 성", false);
    }
    
    // 상세 통계 정보 추가
    eb.addField("소모 메소", mesoFormatWithUnit(n.getMeso()), false);
    eb.addField("강화 횟수", mesoFormat(n.getCount()) + " 회", true);
    eb.addField("성공 횟수", mesoFormat(n.getSuccess()) + " 회", true);
    eb.addField("실패 횟수", mesoFormat(n.getFail()) + " 회", true);
    eb.addField("파괴 횟수", mesoFormat(n.getDestroyed()) + " 회", true);
    
    return eb.build();
}
한국노총대구지역본부 웹 사이트
2025.03 - 2025.06🔗

한국노총 대구지부의 공식 웹 사이트입니다. 웹 사이트 내 수강 신청 기능을 주로 하여, 전체적인 웹 사이트 개발 외주를 진행했습니다. 개발 주요 기능으로는 수강 신청 시스템과 관리자 시스템의 프론트엔드와 백엔드 전체를 개발하였습니다.

팀 구성

1인 개발

담당 역할

프로젝트 설계 프론트엔드 개발 백엔드 개발

React
JavaScript
Python
FastAPI
MongoDB
PHP
Linux
도전과제
문제급변하는 클라이언트 요구사항 대응
해결클라이언트에게 상황을 설명하고, React / FastAPI / MongoDB 기반의 가벼운 풀스택 프로젝트로 변경을 요청함. 프로젝트 설계 내역과 함께 정보를 공유하며 클라이언트를 설득해 개발에 착수할 수 있었음.
결과초기 계약은 Cafe24 기반 단순 수강신청이었으나, 실제 내용은 Cafe24 시스템으로 구현이 불가능했음. 이에 따라 기능을 완성하기 위해 풀스택 개발로 전환해야하는 문제가 발생함.
문제개발 중 지속적인 기능 확장 요청
해결기능별 개발 소요시간과 개발 우선순위를 클라이언트와 공유하며 전체 일정에 끼치는 영향을 투명하게 공개해 현실적인 일정을 조율함. 추가가 확정된 기능은 AI와 Figma를 활용해 목업을 제작하고, 이를 공유하여 기능 개발을 조율할 수 있었음.
결과수강신청 시스템에서 시작하여, 이를 관리할 관리자 페이지의 추가, 관리자 페이지 내 회원 관리 기능 추가, 팝업 관리 기능 추가, 통계 조회 기능 추가 등 점진적으로 기능 확장을 요청하는 문제가 있었음.
문제기존 PHP 시스템과의 연동 복잡성
해결iframe 통신 코드를 PHP 및 React 프로젝트에 구현하였고, 이를 통해 세션을 공유하는 매커니즘을 구축하여 해결함.
결과Cafe24라는 특성 상 PHP 코드 기반의 시스템과 새로 개발한 React 앱 간 데이터 연동이 필요한 문제가 발생.
문제실무 환경 구축 및 배포 최적화
해결Ubuntu 서버 환경에서 Nginx를 통해 프론트와 api 서버를 분리하고, certbot을 이용해 SSL 인증서를 적용함.
결과HTTPS 설정, 자동 인증 갱신, Nginx 트래픽 라우팅, 보안 설정 등 실제 서비스 환경을 구성하는 데에 어려움이 있었음.
문제짧은 개발 기간 내 고품질 결과물 요구
해결기능별 우선순위를 정리해 클라이언트와 공유하고, 각 컴포넌트 및 기능을 모두 분리하여 개발이 완료되는 즉시 클라이언트에게 요청해 QA를 진행함. 빠른 피드백 반영과 개발을 동시에 진행해 3개월이라는 시간 내에 결과물을 완성할 수 있었음.
결과지속적인 요구사항 변경에도 불구하고 클라이언트의 서비스 오픈 일정으로 인해 개발 기간이 약 2주 단축되어야 하는 상황이 발생함.
코드 보기 (2)
수강신청 로직 내 일부(javascript)동적으로 폼 내용을 검증하여 사용자 경험을 최적화하였습니다.
// 숫자만 입력 가능하도록 하는 함수 (생년월일, 전화번호)
const handleNumericInput = (e, fieldName, maxLength, nextFieldRef) => {
    const { value } = e.target;
    const numericValue = value.replace(/[^0-9]/g, "");

    setFormData((prevState) => ({
        ...prevState,
        [fieldName]: numericValue,
    }));

    // 최대 길이에 도달하면 다음 필드로 포커스 이동
    if (numericValue.length === maxLength && nextFieldRef && nextFieldRef.current) {
        nextFieldRef.current.focus();
    }
};

// 폼 유효성 검사
const validateForm = () => {
    const newErrors = {};
    
    // 차량번호 검증 (특별 처리)
    if (formData.carNumber && formData.carNumber.trim() === "없음") {
        // "없음"은 유효한 값이므로 에러에서 제거
        if (newErrors.carNumber) delete newErrors.carNumber;
    } else if (formData.carNumber && formData.carNumber.trim() !== "") {
        const carNumberRegex = /^[\d]{2,3}[가-힣]{1}[\d]{4}$/;
        if (!carNumberRegex.test(formData.carNumber.replace(/\s+/g, ""))) {
            newErrors.carNumber = "올바른 차량번호 형식이 아닙니다. (예: 12가1234)";
        }
    }
    
    return newErrors;
};
수강신청 승인 페이지 코드 일부(javascript)수강신청 폼과 같은 대용량 데이터 테이블을 최적화하여 UX를 개선하였습니다.
// 상태 우선순위 매핑으로 정렬 최적화
const STATUS_PRIORITY = {
    enrolled: 1,    // 승인대기
    approved: 2,    // 승인완료  
    canceled: 3,    // 승인취소
    complete: 4     // 과정수료
};

// 필터링된 데이터 정렬
const filteredEnrollments = enrollments
    .filter((item) => {
        const nameMatch = enrollmentRequest.applicant_name
            .toLowerCase().includes(nameFilter.toLowerCase());
        const statusMatch = statusFilter === "all" || 
            enrollment.status === getStatusCodeFromFilter(statusFilter);
        const targetAudienceMatch = targetAudienceFilter === "all" || 
            courseSummary.target_audience === targetAudienceFilter;
        
        return nameMatch && statusMatch && targetAudienceMatch;
    })
    .sort((a, b) => {
        // 상태 우선순위에 따라 정렬
        const statusPriorityA = STATUS_PRIORITY[a.enrollment.status] || 999;
        const statusPriorityB = STATUS_PRIORITY[b.enrollment.status] || 999;
        
        if (statusPriorityA !== statusPriorityB) {
            return statusPriorityA - statusPriorityB;
        }
        
        // 같은 상태면 강의 시작일 기준 정렬
        return new Date(a.course_summary.start_date) - new Date(b.course_summary.start_date);
    });
주식Talk
2023.09 - 2023.12

1시간 단위로 뉴스 기사와 주가 등락률을 활용하여, 전일 종가에 비해 당일 종가의 등락률을 예측하는 인공지능 카카오톡 챗봇입니다. Python Flask로 구현한 서버를 Kakao i OpenBuilder에 연결하여 사용자 발화 의도를 분석하여 보다 정밀한 동작이 가능하게끔 설계하였습니다. AI가 생성한 결과를 바탕으로 동적으로 그래프 이미지를 생성하고, 이를 직접 디자인한 카카오톡 메시지에 첨부함으로써 사용자가 정보를 읽는 데에 불편함이 없도록 구현하였습니다.

팀 구성

4인 개발

담당 역할

Kakao i OpenBuilder 내 시나리오 및 스킬 제작 챗봇 통신을 위한 Flask 기반 챗봇 서버 및 콘텐츠 다운로드 서버 구현 주식 분야와 종목의 개체명 인식 및 유사도 분석 기능 구현 동적 그래프 생성 및 카카오톡 내 메시지 디자인 설계 멀티스레딩을 활용해 국내 주식 전 종목에 대한 8개월 분량의 기사 데이터 약 20만 개 이상 크롤링

Kakao i OpenBuilder
Python
Flask
Linux
Java
jsoup
Konlpy
도전과제
문제대용량 데이터 크롤링 및 처리 최적화
해결멀티스레딩을 활용한 병렬 크롤링, 메모이제이션을 통한 중복 크롤링 방지, 효율적인 데이터 파싱 구조를 설계하여 약 2일에 걸쳐 데이터를 수집함.
결과국내 주식 전 종목 대상 8개월 분량 20만 개 이상 기사 데이터를 최대한 빠르게 수집해야 하는 문제가 발생함.
문제제한된 UI에서의 복합 정보 표현
해결단순 수치 및 글로만 표현했을 때보다 이미지가 있을 때 사용자가 쉽게 정보를 받아들임을 인지함. 이에 따라 동적으로 긍/부정도 그래프를 생성하는 기능을 구현함. 관련 기사 데이터는 메시지 외부 버튼으로 분리해 가독성을 향상할 수 있었음.
결과카카오톡 메시지 인터페이스 내에서 여러 정보를 직관적으로 표현하는 데 어려움이 있었음.
문제자연어 처리를 통한 의도 파악 정확도 향상
해결Konlpy를 활용해 형태소를 분석하고 발화 의도를 파악하는 모델에 연결함. 주식 및 종목명 또한 유사도 계산을 통해 모호한 입력을 처리함. 추가로 개체명 인식 기능을 구현해 삼성전자(삼전), KT(케이티)와 같은 줄임말 및 외국어에도 대응할 수 있도록 제작함.
결과사용자가 입력한 종목명이나 분야, 발화 의도를 정확히 인식하지 못하는 경우가 발생함.
문제팀 내 개발 방식 및 우선순위 조율
해결Figma를 통한 시각적 설명, 개발 소요시간 객관적 분석 자료 제시로 합의점을 도출함.
결과AI 기능 향상 우선 vs 병렬 개발에 대한 팀장과의 의견 차이가 발생함.
코드 보기 (1)
메시지 수신부 코드 일부(python)의도 파악 및 발화의 모호함을 해결하기 위해 여러 예외 처리부를 구현하였습니다.
@app.route('/api/message', methods=['POST'])
def message():
    content = request.get_json()
    user_input = content['userRequest']['utterance'].replace("\n", "")
    
    # 의도 파악
    intention_predict = intention_understanding_instance.intentionPrediction(user_input)
    intention_correct = intention_predict in [3, 4, 6]
    
    if not intention_correct:
        return jsonify(makeMessage(data=None, type=4))
    
    # 단어 대체 및 예외 처리
    user_input = alterWords(user_input.replace(" ", "").strip())
    exception_type, exception_word = findException(user_input.replace(" ", "").upper().strip())
    
    # 종목명 및 분야 체크
    if exception_type == -1:
        found_word = checker.check_words(user_input.replace(" ","").upper())
        if found_word is not None:
            try:
                # 분야 데이터 처리
                messageData, stock_count = findSectorData(found_word)
                return jsonify(makeMessage(data=messageData, type=2, count=stock_count))
            except:
                # 종목 데이터 처리
                messageData, news_count = findStockData(found_word)
                return jsonify(makeMessage(data=messageData, type=1, count=news_count))
COCA : 그룹 및 개인 일정 관리 공유 서비스
2024.02 - 2024.06

+1

그룹 및 개인 일정 관리에 도움을 주고, 그룹 내 인원의 빈 일정을 찾아주는 커뮤니티 형식의 공유 협업 캘린더입니다. 직관적인 UI를 설계 및 구현하여 사용자가 이용하기에 어려움이 없는 서비스를 만들고자 하였습니다. 브루트포스와 인터벌 병합 알고리즘의 두 알고리즘을 상황에 맞게 사용하여, 최대한 빠르게 빈 일정을 찾도록 구현하였습니다.

팀 구성

4인 개발

담당 역할

프론트엔드 UI 구현 프론트엔드 Axios 통신 및 데이터 렌더링과 세션 관리 기능 구현 브루트포스와 인터벌 병합 알고리즘을 활용한 빈 일정 찾기 알고리즘 구현 백엔드 CORS 관리 및 컨트롤러 일부 구현

JavaScript
React
Axios
Java
Spring
MySQL
도전과제
문제복잡한 빈 일정 찾기 알고리즘 최적화
해결브루트포스, 인터벌 병합 알고리즘, 이분탐색, 해시 테이블 등 여러 알고리즘을 활용해 프로토타입을 개발하여 속도를 측정함. 상황에 따라 가장 적합한 알고리즘을 택하여 구현함. 이에 단순 날짜 계산은 브루트포스로, 분 단위의 시간 계산에서는 인터벌 병합 알고리즘을 채택해 연산 시간을 10초 이내로 단축함.
결과단순 브루트포스 알고리즘을 활용하는 경우, 여러 사용자의 일정을 종합하여 공통적으로 빈 시간을 찾는 연산이 10분 이상 소요되는 문제가 발생.
문제인증 방식 급변에 따른 소스코드 전면 수정
해결Axios 인터셉터를 제안해 이를 적용, 토큰 자동 관리 시스템을 구현하여 기존 코드를 최대한 보존할 수 있었음.
결과마감 일주일 전 세션 기반의 인증 방식에서 JWT 토큰 인증으로 갑작스러운 변경 요구가 있었음. 충분히 고민해보고 적용해야 할 사항이라는 의견을 제시했지만 팀원의 독단적인 변경으로 인한 문제가 발생함.
문제프론트엔드 - 백엔드 간 데이터 통신 최적화
해결React 내 상태 관리를 useState만 사용하는 것이 아니라 useMemo 또한 적절히 사용하여 정보를 캐시할 수 있도록 하였고, 백엔드 코드 및 서버 정책을 수정해 CORS 에러를 해결할 수 있었음.
결과이미지 서버 및 기타 컨트롤러 내 CORS 이슈, 실시간 일정 업데이트에 대한 렌더링 성능 이슈가 발생함.
문제페이지 이동 시 상태 유지 문제
해결로컬스토리지 및 Redux를 활용하여 상태 관리 시스템을 구축했고, 사용자 경험을 개선할 수 있었음.
결과사용자가 페이지를 이동할 때마다 이전의 작업 내용이 초기화되고 기본값으로 돌아가는 문제가 발생함.
문제팀 내 풀스택 개발자로서의 가교 역할
해결팀 내 유일한 풀스택 개발자로서 소통 창구를 담당해 프론트엔드와 백엔드 각각에서 요구하는 조건이 무엇인지를 취합해 회의를 주도함. 프론트엔드만 구현하는 것이 아니라 필요하다면 Spring 백엔드 컨트롤러 또한 동시에 직접 구현하며 팀 통합을 조율함.
결과개발 진행 중 프론트엔드 - 백엔드 간 소통이 되지 않는 문제가 있었음.
코드 보기 (3)
빈 일정 찾기 알고리즘 함수 일부(java)인터벌 병합 알고리즘을 사용해 성능을 최적화하였습니다.
private List<CommonSchedule> interval(LocalDateTime startTime, LocalDateTime endTime, 
                                    int duration, List<String> members) {
    List<Interval> combined = new ArrayList<>();
    final int timeSlot = 10;
    
    // 모든 멤버의 일정을 수집
    for (String memberId : members) {
        List<PersonalSchedule> personalSchedules = 
            personalScheduleRepository.findPersonalScheduleByDateRange(memberId, startTime, endTime);
        
        if (personalSchedules != null && personalSchedules.size() > 0) {
            for (PersonalSchedule schedule : personalSchedules) {
                combined.add(new Interval(schedule.getStartTime(), schedule.getEndTime()));
            }
        }
    }
    
    // 인터벌 병합으로 중복 제거 및 최적화
    List<Interval> mergeSchedule = IntervalMerge.intervalMerge(combined);
    Collections.sort(mergeSchedule, Comparator.comparing(Interval::getStart));
    
    // 빈 시간 계산
    List<CommonSchedule> resultSchedule = new ArrayList<>();
    LocalDateTime current = startTime;
    
    for (Interval interval : mergeSchedule) {
        while (current.plusMinutes(duration).isBefore(interval.getStart()) || 
               current.plusMinutes(duration).isEqual(interval.getStart())) {
            resultSchedule.add(new CommonSchedule(current, current.plusMinutes(duration)));
            current = current.plusMinutes(timeSlot);
        }
        
        if (current.isBefore(interval.getEnd())) {
            current = interval.getEnd();
        }
    }
    
    return resultSchedule;
}
GroupList 통신 코드 일부(javascript)Axios 인터셉터를 활용한 JWT 토큰 자동 관리 시스템을 구현하였습니다.
// JWT 토큰 자동 갱신 처리
const getGroupList = async (id, navigate) => {
  try {
    const config = {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    };
    const res = await axios.get(
      process.env.REACT_APP_SERVER_URL + `/api/calendar/member/${id}`,
      config
    );
    
    if (res.data.code === 200) {
      return res.data.data;
    } else if (res.data.code === 401 || res.data.data === null) {
      // 토큰 만료 시 자동 갱신 후 재시도
      await refreshAccessToken(navigate);
      return getGroupList(id, navigate);
    } else {
      throw new Error("unknown Error");
    }
  } catch (error) {
    console.error(error);
    Swal.fire({
      position: "center",
      icon: "error", 
      title: "에러!",
      text: "서버와의 통신에 문제가 생겼어요!",
      showConfirmButton: false,
      timer: 1500,
    });
    return [];
  }
};
메인페이지 코드 일부(javascript)로컬스토리지를 활용하여 상태를 관리, UX를 개선할 수 있었습니다.
// 페이지 이동 시 상태 유지
useEffect(() => {
  // 로컬스토리지에서 이전 상태 복원
  const savedState = localStorage.getItem('calendarState');
  if (savedState) {
    const parsedState = JSON.parse(savedState);
    setSelectedDate(parsedState.selectedDate);
    setViewMode(parsedState.viewMode);
  }
}, []);

// 상태 변경 시 자동 저장
useEffect(() => {
  const stateToSave = {
    selectedDate,
    viewMode,
    currentGroup
  };
  localStorage.setItem('calendarState', JSON.stringify(stateToSave));
}, [selectedDate, viewMode, currentGroup]);
제미니 테트리스
2022.09🔗

버츄얼 그룹 RE:REVOLUTION의 팬 게임입니다. 그룹 멤버의 생일을 위한 팬 게임 외주를 맡아 개발했습니다. Unity를 이용해 개발하고, WebGL로 빌드해 웹 상에서 게임을 플레이 할 수 있도록 제작하였습니다. PC뿐 아니라 모바일 환경을 위한 버튼 UI를 추가하여 여러 환경에서도 문제 없이 플레이 할 수 있도록 제작하였습니다.

팀 구성

1인 개발

담당 역할

게임 개발 일체

Unity
C#
WebGL
도전과제
문제크로스 플랫폼 호환성 확보
해결WebGL 빌드를 활용하여 브라우저 기반 플레이 환경을 구축하고, 빌드된 html 파일을 수정하여 반응형 UI를 설계함.
결과PC와 모바일 기기에서 동일한 게임 경험을 제공해야 하는 문제가 발생함.
문제모바일 터치 인터페이스 최적화
해결터치 버튼 UI를 추가로 구현하여 기존 키보드로 조작할 수 있는 기능에 대응함.
결과키보드가 없는 모바일 환경에서도 직관적으로 게임을 조작할 수 있어야 했음.
문제다양한 화면 비율에 대한 블록 좌표 정렬
해결WebGL 컨테이너 크기를 화면 비율에 맞게 동적으로 조정하여 상대적 좌표 시스템을 적용하여 해결함.
결과모바일의 경우, 기종별로 화면 비율이 달라 테트리스 블록 위치가 어긋나는 현상이 있었음.
문제외주 프로젝트 품질 관리
해결클라이언트 피드백을 최대한 빠르게 반영해 반복적으로 개선해나감. 직접 QA 테스트를 진행하며 버그를 최소화 함.
결과1인 개발 및 QA가 존재하지 않는 상황에서의 외주였지만 브랜드 이미지에 맞는 퀄리티가 요구됨.
문제짧은 개발 기간 내 완성도 확보
해결Unity의 기존 에셋을 최대한 활용하고, 핵심 기능 및 메인 게임 로직을 우선 개발한 후 부가 기능을 순차적으로 업데이트 함. 이에 처음엔 노멀 모드 뿐이었지만, 웹이라는 특성 상 배포 후 수정이 가능하여 이지 모드를 추후 추가함.
결과1개월이라는 제한된 시간 내에 완성된 게임을 제작해야 했음.
코드 보기 (1)
게임 로직 코드 일부(csharp)게임 로직 일부 및 레벨 시스템을 구현하였습니다.
// 테트로미노 이동 및 충돌 검사
bool MoveTetromino(Vector3 moveDir, bool isRotate)
{
    Vector3 oldPos = tetrominoNode.transform.position;
    Quaternion oldRot = tetrominoNode.transform.rotation;

    tetrominoNode.transform.position += moveDir;
    if (isRotate) {
        tetrominoNode.transform.rotation *= Quaternion.Euler(0, 0, 90);
    }

    if (!CanMoveTo(tetrominoNode)) {
        // 이동 불가능하면 원래 위치로 복구
        tetrominoNode.transform.position = oldPos;
        tetrominoNode.transform.rotation = oldRot;

        // 아래로 떨어지다가 막힌 경우 보드에 추가
        if ((int)moveDir.y == -1 && (int)moveDir.x == 0 && isRotate == false) {
            AddToBoard(tetrominoNode);
            CheckBoardColumn();
            CreateTetromino();

            // 게임 오버 체크
            if (!CanMoveTo(tetrominoNode)) {
                gameOverPanel.SetActive(true);
            }
        }
        return false;
    }
    return true;
}

// 동적 난이도 조절 시스템
float FallCycleTable()
{
    switch (levelInt) {
        case 1: return isEasyMode ? 2.0f : 1.0f;
        case 2: return isEasyMode ? 1.8f : 0.9f;
        // ... 레벨별 속도 조절
        default: return 0.001f;
    }
}
오뉴런
2023.01 - 2023.08

버츄얼 그룹 RE:REVOLUTION의 팬 게임입니다. 그룹 멤버의 생일을 위한 팬 게임 외주를 맡아 개발했습니다. Unity를 이용해 개발한 쿠키런 형식의 2D 횡 스크롤 달리기 게임입니다.

팀 구성

1인 개발

담당 역할

게임 개발 일체 레벨 디자인 게임 밸런싱

Unity
C#
도전과제
문제Unity 2D 횡스크롤 게임 핵심 매커니즘 구현
해결Unity Animator를 활용한 자연스러운 움직임 및 플로우를 구현하고, 물리 엔진 기반의 점프/슬라이드 시스템을 구현함.
결과쿠키런 스타일의 무한 러닝 게임에서 자연스러운 캐릭터 움직임이 요구됨.
문제게임 밸런스 및 난이도 조절
해결거리별 속도 증가 시스템을 구현하고, 직접 레벨 디자인 및 플레이 테스트를 진행하며 아이템 및 장애물 배치 패턴을 다양화 함. 또한 거리 및 속도, 스테이지에 따른 점수 시스템을 조절해 게임의 밸런스를 맞춤.
결과플레이어가 지루하지 않으면서도 적절한 도전감을 느낄 수 있는 난이도 설계가 요구됨.
코드 보기 (3)
게임매니저 코드 내 일부(csharp)싱글톤 패턴을 활용하여 게임 매니저를 구현하였습니다.
// GameManager.cs - 게임 상태 관리
public class GameManager : MonoBehaviour
{
    private static GameManager _instance;
    private bool gameOver = false;
    private int stage = 0;
    private bool isStageChanged = false;
    
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType(typeof(GameManager)) as GameManager;
            }
            return _instance;
        }
    }
    
    // 게임 상태 프로퍼티들
    public bool GameOver { get; set; }
    public int Stage { get; set; }
    public bool IsStageChanged { get; set; }
}
장애물 생성 시스템 내 코드 일부(csharp)랜덤으로 패턴을 생성하되, 오브젝트 풀링을 통해 성능을 최적화하였습니다.
// PatternManager.cs - 패턴 기반 장애물 생성
void Update()
{
    switch (GameManager.Instance.Stage)
    {
        case 0: min = 0; max = 4; break;
        case 1: min = 5; max = 9; break;
        case 2: min = 10; max = 13; break;
    }
    
    if (GameManager.Instance.IsLandDestroyed)
    {
        if (GameManager.Instance.IsStageChanged)
        {
            // 스테이지별 시작 패턴 생성
            switch (GameManager.Instance.Stage)
            {
                case 0:
                    GameObject newPattern1 = Instantiate(startPattern1, 
                        new Vector3(18f, 0.1f, 0), Quaternion.identity);
                    break;
                // ... 다른 스테이지들
            }
        }
        else
        {
            // 랜덤 패턴 생성
            GameObject newPattern = Instantiate(
                patternList[Random.Range(min, max)], 
                new Vector3(18f, 0.1f, 0), Quaternion.identity);
        }
        GameManager.Instance.IsLandDestroyed = false;
    }
}
스크롤 컨트롤러 코드 내 일부(csharp)플레이어 캐릭터의 위치 이동 및 스크롤 초과를 방지하기 위해 무한 스크롤 시스템을 구현하였습니다.
// ScrollController.cs - 백그라운드 무한 스크롤
void Update()
{
    if (!GameManager.Instance.GameOver)
    {
        transform.Translate(-mapSpeed * Time.deltaTime, 0, 0);
        
        if (transform.position.x < ifposX)
        {
            transform.Translate(posX, 0, 0); // 위치 리셋으로 무한 스크롤
        }
    }
}

Skills & Technologies

Frameworks & Libraries

React
Vue.js
Discord API
FastAPI
Axios
MUI
Node.js
Flask
Express
Spring

Languages

Java
Kotlin
JavaScript
TypeScript
HTML/CSS
C#
C++
Python
PHP
SQL

Tools & Platforms

Git
Unity
Linux
Claude Code
MongoDB
Slack
Discord
Mattermost
Teams
Figma
MySQL

Achievements & Licenses

수상

2023-11

KIT Engineering Fair 동상

·국립금오공과대학교 LINC3.0 사업단

활동

2024-01

CO-UP CAMPUS CHATBOT HACKATHON 참가

·교육부 외 6개 기관

자격증

2024-09

정보처리기사

·한국산업인력공단
2025-01

JLPT N2

·일본국제교류기금

Education

2025.07현재

SSAFY

14기 · 모바일트랙

2019.032025.02

국립금오공과대학교

공학사 · 컴퓨터소프트웨어공학과

2016.032019.02

대구시지고등학교

고등학교 졸업 · 이과

함께 일하고 싶어요!

프로젝트 문의 및 협업 제안, 입사 제안을 기다립니다.

© 2026 이상헌. All rights reserved.

Built with Next.js, FastAPI & Claude Code.