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

Lee Sang-Heon

Full Stack Developer & Game Programmer

Experience

Research Engineer
·Wellbia.com Co., Ltd.·2021.12 – 2023.02

A company developing Xigncode3, an anti-cheat module and program for games. · Applied C++ security modules to Unity and Android platforms · Implemented cross-platform integration between C++ and Java using JNI framework · Discovered and proposed countermeasures for global offset vulnerabilities through Unreal Engine (UE4) source code analysis · Researched ACTk Obscured Variable analysis and memory tampering prevention mechanisms · Built development environment automation scripts using Windows PowerShell

Vice President & Reverse Engineering Mentor
·Kumoh National Institute of Technology Information Security Club BOSS·2023.03 – 2024.08

Provided mentoring to juniors based on game security knowledge acquired while working. · Designed reverse engineering curriculum and created practical materials · Mentored static analysis using debugging tools (IDA) · Mentored dynamic analysis using memory tampering tools (Cheat Engine, etc.) · Conducted basic security concept education accessible to non-majors · Organized internal CTF and created dynamic analysis-related challenges

Undergraduate Researcher
·Signal Processing and Intelligent Network Lab·2023.07 – 2025.02

Participated in various laboratory projects as an undergraduate researcher. · Frontend development within projects · UI/UX design within projects · Research on integrating markdown editors and formula editors


Projects

AngelicBusterBot(Abbot)
2022.04 - Currently in Service🔗

+1

A Discord chatbot that provides MapleStory-related information to users. Utilizes Nexon Open API to deliver real-time game information, character search, and guild details. Has been in service for approximately 3 years, currently serving over 180,000 users across about 4,700 servers.

Team

1-Person Development

Role

Chatbot and website development Operations and maintenance

Java
JDA
Maven
Nexon Open API
jsoup
REST API
Gson
Linux
JavaScript
FastAPI
React
MUI
axios
Challenges
ProblemResponse delays due to large-scale concurrent request processing
SolutionIntroduced sharding-based thread distribution across servers and asynchronous processing to allow each command to be executed independently. This approach reduced response time to approximately 1-2 seconds.
ResultAs a MapleStory Partner and with the surge in game users, bot usage increased dramatically. This led to response delays of over 5 seconds, failing to meet Discord's 3-second response requirement. Users experienced situations where they entered commands but received no response.
ProblemEnsuring stability with limited server resources
SolutionModified to cache reusable resources. Built cron-based real-time memory monitoring system and automatic restart system to resolve issues. This approach achieved uptime of over 98%. Additionally, we completed data migration after upgrading the server.
ResultOOM errors frequently occurred on a server with 1-core CPU and 1GB memory. This caused the bot server to shut down, resulting in low uptime.
ProblemImproving non-intuitive command input method
SolutionStrengthened feedback for incomplete input fields to prevent users from missing required data. Also created numerous exception handling branches to process errors across as many scenarios as possible. Through this approach, related inquiries decreased by more than 80% compared to before.
ResultUsers struggled with how to use the commands, leading to frequent related inquiries.
ProblemImproving user experience within a constrained UI
SolutionConducted direct QA testing through extensive trial and error, researching the most intuitive way to present data. Selected about 3 presentation method cases and gathered opinions from actual users. Through this approach, all essential information could be included without compromising the UX.
ResultDue to the constraint that information could only be expressed via Discord messages, providing too much information became a problem that harmed user experience.
Show Code (5)
Notice Reminder Code Excerpt(java)Implemented large-scale server distribution processing through sharding.
// 스레드풀 크기별 채널 분할하여 병렬 처리
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();
                }
            );
        }
    });
}
Character Information Command Code Excerpt(java)Code for integrating Nexon Open API and parsing data.
// 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";
    }
}
EXP History Command Code Excerpt(java)This is the experience point calculation logic to provide experience point increase.
// 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();
}
Star Force Command Code Excerpt(java)This is a code that processes asynchronous tasks and provides user feedback after receiving a command.
// 슬래시 커맨드 처리 - 스타포스 시뮬레이터
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);
}
Star Force Embed Generation Command Excerpt(java)This is a part of the code that generates a message to the user after simulation.
// 스타포스 시뮬레이션 결과 임베드 생성
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();
}
Federation of Korean Trade Unions Daegu Regional Headquarters Website
2025.03 - 2025.06🔗

Official website for the Daegu branch of the Federation of Korean Trade Unions. Mainly focused on course registration functionality, handled overall website development as commissioned work. Key development features included full frontend and backend development of the course registration system and admin system.

Team

1-Person Development

Role

Project design Frontend development Backend development

React
JavaScript
Python
FastAPI
MongoDB
PHP
Linux
Challenges
ProblemRespond to rapidly changing client requirements
SolutionWe explained the situation to the client and requested a change to a lightweight full-stack project based on React / FastAPI / MongoDB. We were able to persuade the client and begin development by sharing information along with the project design details.
ResultThe initial contract was a simple course registration based on Cafe24, but the actual content was impossible to implement with the Cafe24 system. As a result, the problem of having to switch to full-stack development to complete the function arose.
ProblemRequest for continuous feature expansion during development
SolutionWe share the development time and development priorities for each function with the client and adjust a realistic schedule by transparently disclosing the impact on the overall schedule. For features confirmed to be added, mockups were created using AI and Figma, and these were shared to coordinate feature development.
ResultStarting with the course registration system, there was a problem with requests for gradual expansion of functions, such as adding an administrator page to manage it, adding a member management function within the administrator page, adding a pop-up management function, and adding a statistics inquiry function.
ProblemComplexity of linking with existing PHP system
SolutionThe iframe communication code was implemented in the PHP and React projects, and a mechanism for sharing sessions was established to solve the problem.
ResultDue to the nature of Cafe24, a problem occurred that required data linking between the PHP code-based system and the newly developed React app.
ProblemOptimization of practical environment construction and distribution
SolutionIn the Ubuntu server environment, separate the front and api servers through Nginx and apply the SSL certificate using certbot.
ResultThere were difficulties in configuring the actual service environment, including HTTPS settings, automatic authentication renewal, Nginx traffic routing, and security settings.
ProblemRequires high quality results within a short development period
SolutionWe organize the priorities for each function and share them with the client, separate each component and function, and request it from the client as soon as development is completed to proceed with QA. By quickly reflecting feedback and conducting development simultaneously, we were able to complete the result within 3 months.
ResultDespite continuous requirements changes, a situation arose where the development period had to be shortened by about two weeks due to the client's service opening schedule.
Show Code (2)
Course Registration Logic Excerpt(javascript)Optimized user experience by dynamically validating form content.
// 숫자만 입력 가능하도록 하는 함수 (생년월일, 전화번호)
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;
};
Course Registration Approval Page Code Excerpt(javascript)UX has been improved by optimizing large data tables such as course registration forms.
// 상태 우선순위 매핑으로 정렬 최적화
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);
    });
StockTalk
2023.09 - 2023.12

An AI-powered KakaoTalk chatbot that predicts daily closing price fluctuations compared to the previous day's closing price using hourly news articles and stock price movements. Implemented with a Python Flask server connected to Kakao i OpenBuilder to analyze user speech intent for more precise operations. Dynamically generates graph images based on AI-generated results and attaches them to custom-designed KakaoTalk messages for seamless user experience.

Team

4-Person Development

Role

Scenario and skill creation in Kakao i OpenBuilder Implementation of a Flask-based chatbot server and content download server for chatbot communication Implemented entity recognition and similarity analysis for stock sectors and tickers Dynamic graph generation and KakaoTalk message design Crawled over 200,000 stock news articles covering 8 months across all domestic tickers using multithreading

Kakao i OpenBuilder
Python
Flask
Linux
Java
jsoup
Konlpy
Challenges
ProblemOptimize large data crawling and processing
SolutionData was collected over approximately two days by designing parallel crawling using multithreading, preventing duplicate crawling through memoization, and designing an efficient data parsing structure.
ResultA problem arose in collecting data on more than 200,000 articles covering all domestic stocks as quickly as possible over 8 months.
ProblemComplex information representation in constrained UI
SolutionIt is recognized that users accept information more easily when there are images than when it is expressed only through simple numbers and text. Accordingly, a function to dynamically generate a positive/negative graph is implemented. Related article data could be separated into buttons outside the message to improve readability.
ResultThere was difficulty in intuitively expressing various information within the KakaoTalk message interface.
ProblemImproved accuracy of intent identification through natural language processing
SolutionUsing Konlpy, morphemes are analyzed and connected to a model that understands the intention of speech. Stock and item names are also processed for ambiguous input through similarity calculation. In addition, the entity name recognition function was implemented so that it can respond to abbreviations such as Samsung Electronics (Samjeon) and KT(KT) and foreign languages.
ResultThere may be cases where the item name, field, or utterance intention entered by the user is not accurately recognized.
ProblemCoordination of development methods and priorities within the team
SolutionA consensus was reached through visual explanation through Figma and presentation of objective analysis data on development time.
ResultAI function improvement First, there was a difference of opinion with the team leader regarding parallel development of vs.
Show Code (1)
Message Receiver Code Excerpt(python)We implemented several exception handling units to understand intent and resolve ambiguity in utterances.
@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 : Group and Personal Schedule Management Sharing Service
2024.02 - 2024.06

+1

A community-based collaborative calendar that helps manage group and personal schedules and finds available time slots among group members. Designed and implemented an intuitive UI to create a user-friendly service. Utilized both brute-force and interval merging algorithms appropriately to find available time slots as quickly as possible.

Team

4-Person Development

Role

Frontend UI implementation Implemented frontend Axios communication, data rendering, and session management Implemented an available-time search algorithm using brute force and interval merging Backend CORS management and partial controller implementation

JavaScript
React
Axios
Java
Spring
MySQL
Challenges
ProblemOptimizing complex empty schedule finding algorithm
SolutionSpeed ​​was measured by developing a prototype using several algorithms such as brute force, interval merge algorithm, binary search, and hash table. The most appropriate algorithm is selected and implemented depending on the situation. Accordingly, simple date calculations use brute force, and time calculations in minutes use an interval merging algorithm, reducing the calculation time to less than 10 seconds.
ResultWhen using a simple brute force algorithm, a problem arises where the calculation to find a common free time by combining the schedules of multiple users takes more than 10 minutes.
ProblemComplete revision of source code due to rapid change in authentication method
SolutionBy proposing and applying the Axios interceptor, we were able to preserve the existing code as much as possible by implementing an automatic token management system.
ResultA week before the deadline, there was a sudden request to change from session-based authentication to JWT token authentication. Although we expressed our opinion that this was something that needed to be considered and applied, problems arose due to arbitrary changes by team members.
ProblemOptimize data communication between frontend and backend
SolutionReact not only uses useState to manage my state, but also uses useMemo appropriately to cache information, and I was able to resolve the CORS error by modifying the backend code and server policy.
ResultCORS issues within the image server and other controllers, as well as rendering performance issues for real-time schedule updates.
ProblemProblem maintaining state when moving pages
SolutionWe were able to build a status management system using local storage and Redux and improve user experience.
ResultEvery time the user moves between pages, the previous work is reset and returns to the default.
ProblemBridge role as a full-stack developer within the team
SolutionAs the only full-stack developer in the team, I am in charge of the communication channel and lead the meeting by collecting the conditions required from each front-end and back-end. Not only does it implement the front end, but if necessary, it also directly implements the Spring back end controller and coordinates team integration.
ResultDuring development, there was a problem with communication between frontend and backend.
Show Code (3)
Available Time Search Algorithm Function Excerpt(java)Performance was optimized using an interval merging algorithm.
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 Communication Code Excerpt(javascript)We have implemented the JWT token automatic management system using the Axios interceptor.
// 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 [];
  }
};
Main Page Code Excerpt(javascript)We were able to improve UX by managing its status using local storage.
// 페이지 이동 시 상태 유지
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]);
Gemini Tetris
2022.09🔗

A fan game for the virtual group RE:REVOLUTION. Developed as a commissioned birthday fan game for a group member. Developed using Unity and built with WebGL to allow gameplay on the web. Added button UI for mobile environments in addition to PC to ensure playability across various platforms.

Team

1-Person Development

Role

End-to-end game development

Unity
C#
WebGL
Challenges
ProblemEnsure cross-platform compatibility
SolutionWe built a browser-based play environment using the WebGL build, and designed a responsive UI by modifying the built html file.
ResultThe problem of providing the same gaming experience on PC and mobile devices arose.
ProblemOptimized mobile touch interface
SolutionThe touch button UI is additionally implemented to correspond to functions that can be operated with the existing keyboard.
ResultThe game had to be intuitive to operate even in a mobile environment without a keyboard.
ProblemAlign block coordinates for different aspect ratios
SolutionWebGL Solved by applying a relative coordinate system by dynamically adjusting the container size to match the screen ratio.
ResultIn the case of mobile devices, the screen ratio was different for each model, so the positions of Tetris blocks were misaligned.
ProblemOutsourcing project quality management
SolutionWe reflect client feedback as quickly as possible and make iterative improvements. Minimize bugs by directly conducting QA tests.
ResultAlthough it was a one-person development and outsourcing in a situation where QA did not exist, quality that matched the brand image was required.
ProblemEnsure completeness within a short development period
SolutionMake the most of Unity's existing assets, develop core features and main game logic first, and then update additional features sequentially. Accordingly, at first there was only normal mode, but due to the nature of the web, modifications were possible after distribution, so easy mode was added later.
ResultWe had to produce a complete game within a limited time of one month.
Show Code (1)
Game Logic Code Excerpt(csharp)Part of the game logic and level system have been implemented.
// 테트로미노 이동 및 충돌 검사
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;
    }
}
Onyurun
2023.01 - 2023.08

A fan game for the virtual group RE:REVOLUTION. Developed as a commissioned birthday fan game for a group member. A Cookie Run-style 2D side-scrolling runner game developed using Unity.

Team

1-Person Development

Role

End-to-end game development Level design Game balancing

Unity
C#
Challenges
ProblemUnity 2D Implementation of the core mechanism of the side-scrolling game
SolutionRealizes natural movement and flow using Unity Animator and implements a jump/slide system based on a physics engine.
ResultNatural character movement is required in a Cookie Run-style endless running game.
ProblemGame balance and difficulty adjustment
SolutionWe implemented a speed increase system by distance, conducted level design and play testing, and diversified item and obstacle placement patterns. In addition, the score system is adjusted according to distance, speed, and stage to balance the game.
ResultA difficulty design that allows players to feel an appropriate sense of challenge without being boring is required.
Show Code (3)
Game Manager Code Excerpt(csharp)A game manager was implemented using the singleton pattern.
// 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; }
}
Obstacle Generation System Code Excerpt(csharp)Patterns are generated randomly, but performance is optimized through object pooling.
// 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;
    }
}
Scroll Controller Code Excerpt(csharp)An infinite scroll system was implemented to prevent the player character's position movement and scrolling excessively.
// 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

Awards

2023-11

KIT Engineering Fair Bronze Prize

·Kumoh National Institute of Technology LINC3.0 Project Group

Activities

2024-01

CO-UP CAMPUS CHATBOT HACKATHON Participation

·Ministry of Education and 6 Other Organizations

Licenses

2024-09

Information Processing Engineer

·Human Resources Development Service of Korea
2025-01

JLPT N2

·Japan Foundation

Education

2025.07Present

SSAFY

14th Cohort · Mobile Track

2019.032025.02

Kumoh National Institute of Technology

Bachelor of Engineering · Department of Computer Software Engineering

2016.032019.02

Daegu Siji High School

High School Diploma · Science Track

Let's Work Together

Open to project inquiries, collaboration proposals, and job opportunities.

© 2026 Lee Sang-Heon. All rights reserved.

Built with Next.js, FastAPI & Claude Code.