Experience
Research Engineer
·Wellbia.com Co., Ltd.·2021.12 – 2023.02A 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.08Provided 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.02Participated 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





+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.
1-Person Development
Chatbot and website development Operations and maintenance
// 스레드풀 크기별 채널 분할하여 병렬 처리
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();
}
);
}
});
}// 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";
}
}// 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();
}// 슬래시 커맨드 처리 - 스타포스 시뮬레이터
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);
}// 스타포스 시뮬레이션 결과 임베드 생성
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();
}



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.
1-Person Development
Project design Frontend development Backend development
// 숫자만 입력 가능하도록 하는 함수 (생년월일, 전화번호)
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;
};// 상태 우선순위 매핑으로 정렬 최적화
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.
4-Person Development
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
@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))




+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.
4-Person Development
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
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;
}// 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 [];
}
};// 페이지 이동 시 상태 유지
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]);


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.
1-Person Development
End-to-end game development
// 테트로미노 이동 및 충돌 검사
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.
1-Person Development
End-to-end game development Level design Game balancing
// 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; }
}// 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;
}
}// 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
Languages
Tools & Platforms
Achievements & Licenses
Awards
KIT Engineering Fair Bronze Prize
·Kumoh National Institute of Technology LINC3.0 Project GroupActivities
CO-UP CAMPUS CHATBOT HACKATHON Participation
·Ministry of Education and 6 Other OrganizationsLicenses
Information Processing Engineer
·Human Resources Development Service of KoreaJLPT N2
·Japan FoundationEducation
SSAFY
14th Cohort · Mobile Track
Kumoh National Institute of Technology
Bachelor of Engineering · Department of Computer Software Engineering
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.