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

イ・サンホン

Full Stack Developer & Game Programmer

Experience

開発研究員
·(株)ウェルビアドットコム·2021.12 – 2023.02

ゲームのアンチチートモジュールおよびプログラムであるXigncode3を開発する会社です。 · C++で開発されたセキュリティモジュールをUnity、Androidプラットフォームに適用 · JNIフレームワークを活用したC++、Java間クロスプラットフォーム連動実装 · Unreal Engine(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を設計・実装し、ユーザーが利用しやすいサービスを作ることを目指しました。 ブルートフォースとインターバルマージアルゴリズムの2つのアルゴリズムを状況に応じて使用し、可能な限り速く空き時間を見つけるように実装しました。

チーム構成

4人開発

役割

フロントエンドUI実装 フロントエンドAxios通信、データレンダリング、セッション管理機能の実装 ブルートフォースと区間マージアルゴリズムを活用した空き時間検索アルゴリズムの実装 バックエンドCORS管理およびコントローラ一部実装

JavaScript
React
Axios
Java
Spring
MySQL
課題と解決
問題複雑な空のスケジュール検索アルゴリズムの最適化
解決策ブルトフォース、インターバルマージアルゴリズム、二分探索、ハッシュテーブルなど、複数のアルゴリズムを活用してプロトタイプを開発して速度を測定する。 状況に応じて最適なアルゴリズムを選択して実装します。 これに単純日付計算はブルトフォースで、分単位の時間計算ではインターバルマージアルゴリズムを採用して演算時間を10秒以内に短縮する。
結果単純ブルトフォースアルゴリズムを活用する場合、複数のユーザーのスケジュールを総合して共通に空き時間を見つける操作が10分以上かかるという問題が発生。
問題認証方式急変に伴うソースコード前面修正
解決策Axiosインターセプターを提案し、これを適用し、トークン自動管理システムを実装し、既存のコードを最大限保存することができた。
結果締め切り 1週間前、セッションベースの認証方式で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の既存アセットを最大限活用し、コア機能とメインゲームロジックをまず開発した後、アドオンを順次更新する。 これに最初はノーマルモードだけだったが、Webという特性上、配布後修正が可能でイージーモードを今後追加する。
結果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.