Skip to content

Fangsangik/NewsFeeds

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

257 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŽˆ NewsFeed

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

NewsFeed๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์ž‘์€ ์ผ์ƒ์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๋Š” ์†Œ์…œ ๋ฏธ๋””์–ด ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค. ํšŒ์› ๊ฐ€์ž… ํ›„ ๊ฒŒ์‹œ๋ฌผ์„ ์ƒ์„ฑํ•˜๊ณ , ํŒ”๋กœ์šฐ, ์ข‹์•„์š”, ๋Œ“๊ธ€ ๋ฐ ๋ฉ”์‹ ์ € ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์‚ฌ์ง„๊ณผ ์œ„์น˜ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๐Ÿ“Œ ERD

ERD

๐Ÿ› ๏ธ ๊ธฐ์ˆ  ์Šคํƒ

  • Backend: Java, Spring Boot, JPA, QueryDSL, Socket.I/O
  • Database: MySQL, Redis
  • Map Service : KakaoMap API
  • Authentication: JWT, Session, Kakao Login

๐Ÿš€ ํ”„๋กœ์ ํŠธ ๊ธฐ๊ฐ„

  • MVP 1: 2024/12/16 ~ 2024/12/31
  • MVP 2: 2025/02/16 ~ 2024/02/31

๐ŸŽฏ ์ฃผ์š” ๊ธฐ๋Šฅ

1๏ธโƒฃ ํšŒ์›(Member) ๊ด€๋ฆฌ

  • ํšŒ์› ๊ฐ€์ž…: PasswordEncoder๋ฅผ ํ™œ์šฉํ•œ ๋น„๋ฐ€๋ฒˆํ˜ธ ์•”ํ˜ธํ™” ๋ฐ JWT ๋ฐœ๊ธ‰
  • ํšŒ์› ์ •๋ณด ์ˆ˜์ •: ์ˆ˜์ • ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ํ™•์ธ ํ•„์ˆ˜
  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ: OAuth๋ฅผ ์ด์šฉํ•œ ํšŒ์› ๊ฐ€์ž… ๋ฐ ๋กœ๊ทธ์ธ ์ž๋™ ์ฒ˜๋ฆฌ
  • ํšŒ์› ์‚ญ์ œ: Soft Delete ์ ์šฉ, ์‚ญ์ œ ์‹œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ฒ€์ฆ ํ•„์š”
  • ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ: ๊ธฐ์กด ๋น„๋ฐ€๋ฒˆํ˜ธ์™€ ์‹ ๊ทœ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋™์ผํ•˜์ง€ ์•Š๋„๋ก ์ œํ•œ

2๏ธโƒฃ ํ”ผ๋“œ(Feed) ๊ธฐ๋Šฅ

  • ํ”ผ๋“œ ์ƒ์„ฑ: ํšŒ์› PK ๊ฐ’ ๊ฒ€์ฆ ํ›„ ํ”ผ๋“œ ์ž‘์„ฑ
  • ์œ„์น˜ ๊ธฐ๋ฐ˜ ํ”ผ๋“œ ์ž‘์„ฑ: ์œ„์น˜ ์ •๋ณด ๋“ฑ๋ก ๊ฐ€๋Šฅ
  • ํ”ผ๋“œ ์กฐํšŒ: ํšŒ์› ์•„์ด๋”” ๊ธฐ๋ฐ˜ ํ”ผ๋“œ ์กฐํšŒ, ์ธ๊ธฐ ๊ฒŒ์‹œ๋ฌผ(์ข‹์•„์š” ์ˆœ) ์ •๋ ฌ
  • ํ”ผ๋“œ ์ˆ˜์ •: ์œ„์น˜, ์ฃผ์†Œ, ์ด๋ฏธ์ง€, ์ œ๋ชฉ, ๋‚ด์šฉ ์ˆ˜์ • ๊ฐ€๋Šฅ
  • ํ”ผ๋“œ ์‚ญ์ œ: Soft Delete ์ ์šฉ

3๏ธโƒฃ ์นœ๊ตฌ(Friend) ๊ธฐ๋Šฅ

  • ์นœ๊ตฌ ์ถ”๊ฐ€: ์นœ๊ตฌ ์š”์ฒญ ๋ฐœ์‹ ์ž ๊ฒ€์ฆ(Session ์ฒดํฌ), ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€
  • ์นœ๊ตฌ ์š”์ฒญ ์ˆ˜๋ฝ: ์š”์ฒญ ์ˆ˜๋ฝ ์‹œ ACCEPTED ์ƒํƒœ ๋ณ€๊ฒฝ
  • ๋ณด๋‚ธ ์š”์ฒญ ์กฐํšŒ: ์š”์ฒญ ๋ณด๋‚ธ ์‚ฌ๋žŒ ์ •๋ณด ํ™•์ธ
  • ๋ฐ›์€ ์š”์ฒญ ์กฐํšŒ: ์ˆ˜๋ฝ ์ „ ๋ฐ›์€ ์š”์ฒญ ํ™•์ธ ๊ฐ€๋Šฅ
  • ์นœ๊ตฌ ๋ชฉ๋ก ์กฐํšŒ
  • ์นœ๊ตฌ ์‚ญ์ œ

4๏ธโƒฃ ์ข‹์•„์š”(Like) ๊ธฐ๋Šฅ

  • ์ข‹์•„์š” ์ถ”๊ฐ€: ํŠน์ • ํ”ผ๋“œ์— ์ข‹์•„์š” ๋ˆ„๋ฅด๊ธฐ
  • ์ข‹์•„์š” ์ทจ์†Œ: ๊ธฐ์กด์— ๋ˆ„๋ฅธ ์ข‹์•„์š”๋งŒ ์ทจ์†Œ ๊ฐ€๋Šฅ
  • ์ข‹์•„์š” ์ˆ˜ ์กฐํšŒ: ํŠน์ • ํ”ผ๋“œ์˜ ์ข‹์•„์š” ๊ฐœ์ˆ˜ ํ™•์ธ

5๏ธโƒฃ ์ธ์ฆ(Auth) ๋ฐ ๋ณด์•ˆ

  • ์ผ๋ฐ˜ ๋กœ๊ทธ์ธ: Email & Password ์ธ์ฆ ํ›„ JWT ๋ฐœ๊ธ‰
  • ์นด์นด์˜ค ๋กœ๊ทธ์ธ: OAuth2 ๋กœ๊ทธ์ธ ๋ฐ JWT ์žฌ๋ฐœ๊ธ‰
  • JWT + Session ํ˜ผํ•ฉ ์‚ฌ์šฉ: ๋ณด์•ˆ ๊ฐ•ํ™”๋ฅผ ์œ„ํ•ด ์„ธ์…˜๊ณผ JWT๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ธ์ฆ ์ฒ˜๋ฆฌ
  • Interceptor ์ ์šฉ: ์ธ์ฆ ๋ฐ ์ธ๊ฐ€๋ฅผ ์œ„ํ•œ ์ธํ„ฐ์…‰ํ„ฐ ํ™œ์šฉ

6๏ธโƒฃ ๋ฉ”์‹œ์ง€(Message) ๊ธฐ๋Šฅ

  • ๋ฉ”์‹œ์ง€ ์ „์†ก ๋ฐ ์ €์žฅ: Redis๋ฅผ ํ™œ์šฉํ•œ ๋ฉ”์‹œ์ง€ ์ €์žฅ ๋ฐ ์กฐํšŒ
  • WebSocket ๊ธฐ๋ฐ˜ ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ตฌํ˜„

๐Ÿ”ง ๊ธฐ์ˆ ์  ๊ณ ๋ฏผ ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ

MVP1

1๏ธโƒฃ ์ธ์ฆ ๋ฐ ๋ณด์•ˆ ๊ฐ•ํ™”

  • Interceptor + JWT + Session ์กฐํ•ฉ: MVP 1์—์„œ๋Š” Interceptor์™€ Session์„ ์‚ฌ์šฉํ•˜๊ณ , MVP 2์—์„œ Spring Security๋ฅผ ๋„์ž…
  • Refresh Token ๊ด€๋ฆฌ: Redis๋ฅผ ํ™œ์šฉํ•œ ๋ธ”๋ž™๋ฆฌ์ŠคํŠธ ์ ์šฉ
  • ๋กœ๊ทธ์•„์›ƒ ์‹œ Token ์‚ญ์ œ: ํšŒ์› ๋กœ๊ทธ์•„์›ƒ ์‹œ Redis์—์„œ Refresh Token ์ œ๊ฑฐ

2๏ธโƒฃ ์นด์นด์˜ค ๋กœ๊ทธ์ธ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋ฌธ์ œ ํ•ด๊ฒฐ

  • ๋ฌธ์ œ์ : ์นด์นด์˜ค ๋กœ๊ทธ์ธ ์ดํ›„ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์‹œ ์ด๋ฏธ ์‚ฌ์šฉ๋œ Authorization Code๋กœ ์ธํ•ด ์ธ์ฆ ์‹คํŒจ ๋ฐœ์ƒ
  • ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•: code ์š”์ฒญ์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๋ฐ›๋Š”๊ฒƒ์ด ์•„๋‹Œ, ์ง์ ‘ ์นด์นด์˜คํ†ก ์„œ๋ฒ„์— ์š”์ฒญํ•˜๋„๋ก ๋ณ€๊ฒฝ

3๏ธโƒฃ ๋Œ“๊ธ€ ๊ธฐ๋Šฅ ๊ฐœ์„ 

  • ๋Œ€๋Œ“๊ธ€ ๊ณ„์ธต ๊ตฌ์กฐ ์œ ์ง€: ๋ถ€๋ชจ-์ž์‹ ๊ด€๊ณ„๋ฅผ ์œ ์ง€ํ•˜์—ฌ JSON ์‘๋‹ต ๊ตฌ์กฐ ๊ฐœ์„ 
  • Lazy Loading ๋ฌธ์ œ ํ•ด๊ฒฐ: Fetch Join์„ ํ™œ์šฉํ•˜์—ฌ ๋Œ€๋Œ“๊ธ€์„ ํ•œ ๋ฒˆ์˜ ์ฟผ๋ฆฌ๋กœ ๊ฐ€์ ธ์˜ค๋„๋ก ์ˆ˜์ •
@Query("SELECT c FROM Comment c LEFT JOIN FETCH c.children WHERE c.feed.id = :feedId")
List<Comment> findByFeedId(@Param("feedId") Long feedId);

4๏ธโƒฃ ์นœ๊ตฌ ์š”์ฒญ ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€

  • Session ๊ฒ€์ฆ ๋กœ์ง ์ถ”๊ฐ€: ์นœ๊ตฌ ์š”์ฒญ ๋ฐœ์‹ ์ž๊ฐ€ ๋ณธ์ธ์ด ๋งž๋Š”์ง€ ํ™•์ธ
  • ์ค‘๋ณต ์š”์ฒญ ๋ฐฉ์ง€: ์ด๋ฏธ ์นœ๊ตฌ ๊ด€๊ณ„์ธ ๊ฒฝ์šฐ ์š”์ฒญ ๋ถˆ๊ฐ€
if (!authenticatedMemberId.equals(friendRequestDto.getReceiverId())) {
    throw new NoAuthorizedException(ErrorCode.NO_AUTHOR);
}

MVP2

๐Ÿ“Œ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

NewsFeed MVP 2์—์„œ๋Š” Spring Security๋ฅผ ๋„์ž…ํ•˜์—ฌ ๊ธฐ์กด์˜ Interceptor ๋ฐ Session ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•˜๊ณ , ๋ณด์•ˆ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ๊ฐ•ํ™”ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ› ๏ธ ์ฃผ์š” ๋ณ€๊ฒฝ ์‚ฌํ•ญ

1๏ธโƒฃ Spring Security ์ ์šฉ

  • securityFilterChain: ์ธ์ฆ์ด ํ•„์š”ํ•˜์ง€ ์•Š์€ ๋ถ€๋ถ„์„ ์ œ์™ธํ•˜๊ณ , ๋กœ๊ทธ์ธ ํ›„ ์ธ์ฆ์ด ํ•„์š”ํ•œ ๊ธฐ๋Šฅ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„
  • roleHierarchy ์„ค์ •: ์—ญํ•  ๊ณ„์ธต์„ ์ •์˜ํ•˜์—ฌ ๊ถŒํ•œ ๊ด€๋ฆฌ ์ฒด๊ณ„ ๊ฐ•ํ™”
  • Session ์ œ๊ฑฐ ๋ฐ JWT ๊ธฐ๋ฐ˜ ์ธ์ฆ ์ ์šฉ: ๊ธฐ์กด Session ๊ธฐ๋ฐ˜ ์ธ์ฆ์„ ์ œ๊ฑฐํ•˜๊ณ , AuthenticatedMemberUtil์„ ํ™œ์šฉํ•˜์—ฌ ์ธ์ฆ ์ •๋ณด ์ œ๊ณต

2๏ธโƒฃ Controller ๋‚ด ์ค‘๋ณต ์ฝ”๋“œ ์ œ๊ฑฐ ๊ณ ๋ฏผ

  • ๊ธฐ์กด ๋ฐฉ์‹: Long memberId = AuthenticatedMemberUtil.getAuthenticatedMemberId();๋ฅผ ๊ฐ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœ
  • ๊ฐœ์„  ๋ฐฉํ–ฅ:
    • @Authenticationํ™œ์šฉ : ์ธ์ฆ์ด ํ•„์š”ํ•œ ์„œ๋น„์Šค์˜ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ UserDetails๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ง์ ‘ ๋ฐ›์•„, ์ธ์ฆ๋œ Member ์ •๋ณด๋ฅผ ์ถ”์ถœํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณ€๊ฒฝ.
    • ์ด๋ฅผ ํ†ตํ•ด, ๊ธฐ์กด์˜ AuthenticatedMemberUtil.getAuthenticatedMemberId();๋ฅผ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋Š” ๋ฐฉ์‹์„ ๊ฐœ์„ ํ•˜๊ณ , ์ธ์ฆ ๊ด€๋ จ ๋กœ์ง์„ Util๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ ์ปจํŠธ๋กค๋Ÿฌ์˜ ์—ญํ• ์„ ๋‹จ์ˆœํ™”ํ•จ.

3๏ธโƒฃ Spring Security ๋„์ž… ์ด์œ 

โœ… ๋ณด์•ˆ ๊ฐ•ํ™”: CORS, CSRF ๋ณดํ˜ธ ๋ฐ ์ธ์ฆ ์ธ๊ฐ€ ์ฒด๊ณ„ ๊ฐœ์„ 
โœ… ์œ ์ง€๋ณด์ˆ˜ ์šฉ์ด์„ฑ: ๊ฐœ๋ณ„ Filter ํ˜น์€ Interceptor๋ฅผ ๊ณ„์† ์ถ”๊ฐ€ํ•  ๊ฒฝ์šฐ ๊ตฌ์กฐ๊ฐ€ ๋ณต์žกํ•ด์งˆ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ์Œ โ†’ Security๋ฅผ ๋„์ž…ํ•˜์—ฌ ์ธ์ฆ ์ธ๊ฐ€ ๋กœ์ง์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌ
โœ… ๊ตฌํ˜„ ์šฉ์ด์„ฑ: ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ์ง์ ‘ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค Security ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋น ๋ฅด๊ณ  ์•ˆ์ •์ ์ธ ์ธ์ฆ ์ธ๊ฐ€ ์‹œ์Šคํ…œ ๊ตฌ์ถ• ๊ฐ€๋Šฅ

4๏ธโƒฃย WebSocket + Redis ์‚ฌ์šฉ

  • WebSocket๋งŒ ์‚ฌ์šฉ์‹œ Scale Out์‹œ ํ™•์žฅ์ด ์–ด๋ ค์›€
  • Redis๋ฅผ ์‚ฌ์šฉํ•ด Scale Out์‹œ ์ด์  ํ™•๋ณด

๐Ÿ”ง Trouble Shooting

1๏ธโƒฃ Session ์ œ๊ฑฐ ํ›„ ์ธ์ฆ ๋ฌธ์ œ ๋ฐœ์ƒ

๋ฌธ์ œ์ : Security๋ฅผ ์ •ํ™•ํžˆ ํ™œ์šฉํ•˜์ง€ ๋ชปํ•œ ์ƒํƒœ์—์„œ Session ๊ฐ’์„ ๊ฑท์–ด๋‚ธ ๊ฒฐ๊ณผ, SecurityContextHolder๊ฐ€ ์•„๋‹Œ RequestAttribute๋ฅผ ํ†ตํ•ด ๊ฒ€์ฆํ•˜๋ ค๋‹ค ์ธ์ฆ ์‹คํŒจ ๋ฐœ์ƒ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

  • @AuthenticationPrincipal์„ ๊ฐ ์ปจํŠธ๋กค๋Ÿฌ๋งˆ๋‹ค ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ• ๊ณ ๋ ค
  • ํ•˜์ง€๋งŒ ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋งŽ์•„ Util ํด๋ž˜์Šค๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ œ๊ณต

2๏ธโƒฃ ์นœ๊ตฌ ์š”์ฒญ ์‹œ ์ž˜๋ชป๋œ ๊ฒ€์ฆ ๋กœ์ง ๋ฌธ์ œ

๋ฌธ์ œ์ : ๋กœ๊ทธ์ธํ•œ ์‚ฌ์šฉ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋„ ์นœ๊ตฌ ์š”์ฒญ ๋ฐ ์ˆ˜๋ฝ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ์Œ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

  • FriendService์—์„œ ์š”์ฒญ์„ ๊ฒ€์ฆํ•˜๋Š” validate ๋กœ์ง ์ถ”๊ฐ€
if (!authenticatedMemberId.equals(friendRequestDto.getReceiverId())) {
    throw new NoAuthorizedException(ErrorCode.NO_AUTHOR);
}

3๏ธโƒฃ Security์—์„œ userId๊ฐ€ ์•„๋‹Œ username์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๋ฌธ์ œ

๋ฌธ์ œ์ : ๊ธฐ์กด์—๋Š” userId๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ธ์ฆ ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ–ˆ์ง€๋งŒ, Security์—์„œ๋Š” username์„ ๊ธฐ๋ณธ ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ธ์ฆ ์‹คํŒจ ๋ฐœ์ƒ.

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:

  • UserDetailsService์—์„œ username์„ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •
  • ํ•„์š” ์‹œ userId๋ฅผ ์ปค์Šคํ…€ ํ•„๋“œ๋กœ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ด€๋ฆฌ

4๏ธโƒฃ PK vs Email ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ณ ๋ฏผ

๊ณ ๋ฏผ ๋‚ด์šฉ:

  • Email ๊ธฐ๋ฐ˜ ์ธ์ฆ: ์‚ฌ์šฉ์ž ์นœํ™”์ ์ด์ง€๋งŒ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์•„ ์‹๋ณ„์ž๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์• ๋งคํ•จ
  • PK ๊ธฐ๋ฐ˜ ์ธ์ฆ: ๋‹จ์ˆœ ์ˆซ์ž๋กœ ๋…ธ์ถœ ์œ„ํ—˜์ด ์žˆ์ง€๋งŒ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ์„ฑ์ด ๋‚ฎ์•„ ์ธ์ฆ ๋ฐ ์—ฐ์‚ฐ ์‹œ ์„ฑ๋Šฅ ๋ฉด์—์„œ ์œ ๋ฆฌํ•จ

์„ ํƒํ•œ ๋ฐฉ๋ฒ•:

  • AuthService์—์„œ๋Š” Email์„ ์‚ฌ์šฉํ•˜์—ฌ ์ธ์ฆ ์ง„ํ–‰
  • JWT ๋‚ด์—์„œ PK ๊ฐ’์„ ํฌํ•จํ•˜์—ฌ ๋‚ด๋ถ€์ ์œผ๋กœ PK๋ฅผ ํ™œ์šฉ (์œ ์ผ์„ฑ๊ณผ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors