๐Ÿง‘โ€๐Ÿ’ป Portfolio

๐Ÿ‘‹ ์•ˆ๋…•ํ•˜์„ธ์š”! ์ €๋Š” 6๋…„์ฐจ ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋‘๋™ํ˜„ ์ž…๋‹ˆ๋‹ค

Spring Boot์™€ Next.js๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐฑ์—”๋“œ์™€ ํ”„๋ก ํŠธ์—”๋“œ ๋ชจ๋‘ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์œผ๋ฉฐ,
RESTful API ์„ค๊ณ„, ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ, ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๊ฐœ๋ฐœ, ์‚ฌ์šฉ์ž ์„œ๋น„์Šค ํŽ˜์ด์ง€ ๊ฐœ๋ฐœ, AWS ํด๋ผ์šฐ๋“œ ์ธํ”„๋ผ ์šด์˜๊นŒ์ง€
์‹ค๋ฌด ์ค‘์‹ฌ์˜ ๋‹ค์–‘ํ•œ ๊ฒฝํ—˜์„ ํ†ตํ•ด ์„œ๋น„์Šค์˜ ์‹œ์ž‘๋ถ€ํ„ฐ ์šด์˜๊นŒ์ง€ ์ฑ…์ž„์ง€๋Š” ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค.

๐Ÿ›  ๋ณด์œ  ๊ธฐ์ˆ 

๐Ÿ‘จโ€๐Ÿ’ป Backend

  • Spring Boot / Spring Batch / Spring Security
  • JPA / QueryDSL / MyBatis
  • MySQL / Redis
  • JWT ์ธ์ฆ / OAuth

๐Ÿ–ฅ Frontend

  • Next.js / React
  • React Query / Zustand
  • TailwindCSS / Styled Components
  • Chart.js / Recharts

โ˜ Infra & DevOps

  • AWS EC2 / RDS / S3 / Route 53
  • Jenkins / Nexus
  • Linux ๊ธฐ๋ณธ ์‰˜ ์Šคํฌ๋ฆฝํŠธ / crontab
  • ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ๋ฐ HTTPS ์ธ์ฆ์„œ ์„ค์ •

๐Ÿ”ง ํ˜‘์—… & ๊ธฐํƒ€

  • Git / GitHub / GitLab
  • ์Šฌ๋ž™, ํ…”๋ ˆ๊ทธ๋žจ ์•Œ๋ฆผ ์—ฐ๋™
  • ๊ณ ๊ฐ์‚ฌ ์š”๊ตฌ์‚ฌํ•ญ ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ ๊ฒฝํ—˜

๐Ÿ’ผ ๊ฒฝ๋ ฅ ์š”์•ฝ

ใˆœ์•„ํ†ค

2023.07 ~ ์žฌ์ง์ค‘

์„œ๋น„์Šค๊ฐœ๋ฐœํŒ€ ยท ๋Œ€๋ฆฌ/๋งค๋‹ˆ์ €

  • PASS ์œ ๋ฃŒ ์„œ๋น„์Šค ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์‹ ๊ทœ ์„œ๋น„์Šค ๊ฐœ๋ฐœ
  • ๋ฆฌ์›Œ๋“œ ์‹œ์Šคํ…œ ๋„์ž…์œผ๋กœ ์‚ฌ์šฉ์ž ๋ฆฌํ…์…˜ 20% ์ฆ๊ฐ€
  • ๊ด€๋ฆฌ์ž UI/UX ๊ฐœ์„ ์œผ๋กœ ๋ฐ์ดํ„ฐ ์กฐํšŒ ํšจ์œจ์„ฑ 30% ํ–ฅ์ƒ
  • AWS VPC, RDS, Route 53 ์ธํ”„๋ผ ๊ตฌ์„ฑ
  • Jenkins ๊ธฐ๋ฐ˜ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์ถ•์œผ๋กœ ๋ฐฐํฌ ์‹œ๊ฐ„ 50% ๋‹จ์ถ•
  • LGU+ ํ†ต์‹ ์‚ฌ ๋ฌธ์ž ๋ฐœ์†ก ๋ชจ๋“ˆ ๋ฐ API ์—ฐ๋™

ใˆœ์ด๋…ธํ‹ฐ์›€

2020.07 ~ 2023.03

๊ฐœ๋ฐœํŒ€ ยท ๋Œ€๋ฆฌ

  • PHP โ†’ Spring Boot ์ „ํ™˜์œผ๋กœ ์‹œ์Šคํ…œ ์„ฑ๋Šฅ 40% ํ–ฅ์ƒ
  • Restful API๋กœ ์ •์ฑ… ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ฐœ์„ 
  • MyBatis ํŠœ๋‹์œผ๋กœ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ์†๋„ 35% ๋‹จ์ถ•
  • ๋ชจ๋ฐ”์ผ ์›น์•ฑ ๊ฒฐ์žฌ ์Šน์ธ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ ๋ฐ ์•Œ๋ฆผ ์‹œ์Šคํ…œ ๊ตฌ์ถ•
  • ๊ณ ๊ฐ์‚ฌ ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž์ถ˜ ๋ณด์•ˆ ์†”๋ฃจ์…˜ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•

ใˆœ์—์Šคํ‹ฐ์•„์ด๋„คํŠธ์›์Šค

2018.05 ~ 2020.03

๊ธฐ์ˆ ์ง€์›ํŒ€ ยท ์ฃผ์ž„

  • ๊ณ ๊ฐ์‚ฌ ๋„คํŠธ์›Œํฌ ํ™˜๊ฒฝ ๋ถ„์„ ๋ฐ ์†”๋ฃจ์…˜ ๊ตฌ์ถ•
  • ์žฅ์•  ๋Œ€์‘ ๋ฐ ๋ถ„์„, ์ •์ฑ… ์„ค์ • ๋ณด์™„
  • ๋ฒ„์ „ ํŒจ์น˜ ๋ฐ ์—…๊ทธ๋ ˆ์ด๋“œ ์ˆ˜ํ–‰
  • ๋ฌธ์ œ ์žฌํ˜„ ๋ฐ ์žฅ์•  ๋ณด๊ณ ์„œ ์ž‘์„ฑ

๐ŸŒŸ ์‚ฌ์ด๋“œ ํ”„๋กœ์ ํŠธ - DOOMOLE

์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ๋ฐ˜ ์ •๋ณด ๊ณต์œ  ํ”Œ๋žซํผ. Next.js์™€ Spring Boot๋ฅผ ํ™œ์šฉํ•ด ์ž์œ ๊ฒŒ์‹œํŒ, ์˜ํ™” ์ˆœ์œ„, ์ง€๋„ ๊ฒ€์ƒ‰ ๋“ฑ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ธ์ฆ ๋ฐ ๊ฒŒ์‹œํŒ CRUD, ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ, ๊ฒŒ์‹œ๊ธ€ ์ถ”์ฒœ, ๋Œ“๊ธ€ ์‹œ์Šคํ…œ์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

์ง€๋„ ํŽ˜์ด์ง€

ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๋ง›์ง‘ ์ง€๋„ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

์˜ํ™” ํŽ˜์ด์ง€

TMDB API ๊ธฐ๋ฐ˜ ์˜ํ™” ๋ชฉ๋ก ์กฐํšŒ ๋ฐ ์žฅ๋ฅด ํ•„ํ„ฐ๋ง

์ž์œ ๊ฒŒ์‹œํŒ ๋ชฉ๋ก

์ตœ์‹ ์ˆœ ์ •๋ ฌ ๋ฐ ์ข‹์•„์š” ๊ธฐ๋Šฅ ํฌํ•จ ๊ฒŒ์‹œํŒ ๋ชฉ๋ก

๊ฒŒ์‹œํŒ ์ƒ์„ธ

์ž์œ ๊ฒŒ์‹œํŒ ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ๋ฐ ๋Œ“๊ธ€ ์ž‘์„ฑ UI

๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ

์—๋””ํ„ฐ๋ฅผ ํ™œ์šฉํ•œ ๊ฒŒ์‹œ๊ธ€ ์ž‘์„ฑ ๋ฐ ๋ฏธ๋ฆฌ๋ณด๊ธฐ

ํšŒ์›๊ฐ€์ž…

์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ํ”ผ๋“œ๋ฐฑ์ด ํฌํ•จ๋œ ํšŒ์›๊ฐ€์ž… ํ™”๋ฉด

๋กœ๊ทธ์ธ

๋กœ๊ทธ์ธ ์‹คํŒจ ์‹œ ์‚ฌ์šฉ์ž ์•Œ๋ฆผ ์ฒ˜๋ฆฌ

๋งˆ์ดํŽ˜์ด์ง€

์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ ๋ฐ ์ •๋ณด ์ˆ˜์ • ๊ธฐ๋Šฅ

ํด๋”๊ตฌ์กฐ

NEXT.JS ๊ธฐ๋ฐ˜ Frontend ํด๋” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

ํด๋”๊ตฌ์กฐ

SPRING BOOT ๊ธฐ๋ฐ˜ Backend ํด๋” ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค.

๐Ÿงฉ ์ฝ”๋“œ ์˜ˆ์‹œ - ๊ฒŒ์‹œํŒ ๋ชฉ๋ก ์กฐํšŒ

// React Query๋กœ ์ž์œ ๊ฒŒ์‹œํŒ ๋ฆฌ์ŠคํŠธ ์กฐํšŒ
export function useFreeNoticeListQuery(page: number, sort: string, keyword: string) {
  return useQuery({
    queryKey: ['freeNotice', page, sort, keyword],
    queryFn: () => getFreeNoticeList(page, sort, keyword),
  })
}

// API ์š”์ฒญ ํ•จ์ˆ˜
async function getFreeNoticeList(page: number, sort: string, keyword: string) {
  const query = new URLSearchParams({ page: page, sortType: sort, searchText: keyword })
  const res = await fetch('API_URL/notice?query')

  if (res.ok) return res.json()
  if (res.status === 401) {
    localStorage.clear()
    window.location.href = '/account/login'
  }

  const error = await res.json()
  throw new Error(error.message || 'Fetch error')
}

React Query๋ฅผ ํ™œ์šฉํ•ด ๊ฒŒ์‹œํŒ ๋ฆฌ์ŠคํŠธ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋ฉฐ, ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ์— ๋”ฐ๋ฅธ ๋ฐ์ดํ„ฐ ์บ์‹ฑ ์ „๋žต์„ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๐Ÿงฉ ์ฝ”๋“œ ์˜ˆ์‹œ - ๊ฒŒ์‹œํŒ ๋ชฉ๋ก ์กฐํšŒ (Spring Boot)

// Controller
@GetMapping("/list")
public ResSuccess<ResCommonList<List<ResFreeNotice>>> getFreeNoticeList(
    @RequestParam int page,
    @RequestParam String sortType,
    @RequestParam String searchText) {

    ResCommonList<List<ResFreeNotice>> list = freeNoticeService.getFreeNoticeList(page, sortType, searchText);
    return new ResSuccess<>(list);
}

// Service
public ResCommonList<List<ResFreeNotice>> getFreeNoticeList(int page, String sortType, String searchText) {
    Pageable pageable = PageRequest.of((page - 1), 10, getSort(sortType));
    return selectFreeNoticeList(pageable, searchText);
}

// ์‹ค์ œ ๋ฐ์ดํ„ฐ ์กฐํšŒ
public ResCommonList<List<ResFreeNotice>> selectFreeNoticeList(Pageable pageable, String searchText) {
    long count = freeNoticeRepository.countAllByTitleContains(searchText);
    List<FreeNotice> list = freeNoticeRepository.findAllByTitleContains(searchText, pageable);
    List<ResFreeNotice> dtoList = list.stream().map(freeNotice -> {
        int recommendCount = noticeRecommendRepository.countByFreeNoticeIdx(freeNotice.getFreeNoticeIdx());
        ResFreeNotice dto = NoticeConverter.toResFreeNotice(freeNotice);
        dto.setRecommendCount(recommendCount);
        return dto;
    }).collect(Collectors.toList());

    return new ResCommonList<>(count, dtoList);
}

๊ฒŒ์‹œํŒ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ ์กฐ๊ฑด๊ณผ ํŽ˜์ด์ง€๋„ค์ด์…˜์„ ๊ณ ๋ คํ•ด ํšจ์œจ์ ์œผ๋กœ ์กฐํšŒํ•˜๊ณ , DTO ๋ณ€ํ™˜๊ณผ ์ถ”์ฒœ ์ˆ˜๋ฅผ ํฌํ•จํ•œ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“ฉ Contact

๋” ๋„“์€ ์„ฑ์žฅ์˜ ๊ธฐํšŒ๋ฅผ ํ–ฅํ•ด ๋„์ „ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.
ํ˜‘์—…๊ณผ ๊ธฐ์ˆ ์— ์ง„์‹ฌ์ธ ํŒ€๊ณผ ํ•จ๊ป˜ํ•˜๊ณ  ์‹ถ์Šต๋‹ˆ๋‹ค.

๐Ÿ“ง Email: doo_style@naver.com

๐Ÿ“ฑ Phone: 010-2487-3788