Skip to content

๐Ÿ“„[๋ฌธ์„œ][Auth] ์„ธ์ข…๋ง์‹ธ๋ฏธ ์„œ๋น„์Šค ๋‚ด ์„ธ์ข…๋Œ€ ๋กœ๊ทธ์ธ ์ธ์ฆ ๊ตฌํ˜„ ํ˜„ํ™ฉ ๋ถ„์„ย #941

Description

@Cassiiopeia

๐Ÿ“ ํ˜„์žฌ ๋ฌธ์ œ์ 

  • ์„ธ์ข…๋ง์‹ธ๋ฏธ ์„œ๋น„์Šค์˜ ๋กœ๊ทธ์ธ์€ ์„ธ์ข…๋Œ€ํ•™๊ต๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ณต์‹ OAuth/SSO API๊ฐ€ ์—†์–ด, ์‚ฌ์šฉ์ž๊ฐ€ ์ž…๋ ฅํ•œ ์„ธ์ข…ํฌํ„ธ ํ•™๋ฒˆยท๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ์ง์ ‘ ์„ธ์ข…ํฌํ„ธ์— ๋กœ๊ทธ์ธํ•œ ๋’ค ๊ฒฐ๊ณผ ํŽ˜์ด์ง€(๊ณ ์ „๋…์„œ์ธ์ฆํ˜„ํ™ฉ)๋ฅผ ํฌ๋กค๋ง(์Šคํฌ๋ž˜ํ•‘) ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค.
  • ์ด ๋ฐฉ์‹์€ ์™ธ๋ถ€ ์‚ฌ์ดํŠธ(์„ธ์ข…ํฌํ„ธ)์˜ HTML ๊ตฌ์กฐยท์—”๋“œํฌ์ธํŠธยท์„ธ์…˜ ์ •์ฑ…์— ๊ฐ•ํ•˜๊ฒŒ ๊ฒฐํ•ฉ๋˜์–ด ์žˆ์–ด, ์„ธ์ข…๋Œ€ ์ธก ๋ณ€๊ฒฝ ์‹œ ๋กœ๊ทธ์ธ ์ „์ฒด๊ฐ€ ๊นจ์งˆ ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋‹ค.
  • ๋˜ํ•œ ์ธ์ฆ ์‹ ๋ขฐ์„ฑ์„ ์œ„ํ•ด SSL ์ธ์ฆ์„œ ๊ฒ€์ฆ์„ ์ „๋ฉด ๋น„ํ™œ์„ฑํ™”ํ•˜๋Š” ๋“ฑ ๋ณด์•ˆ์ƒ ์งš์–ด์•ผ ํ•  ์ง€์ ์ด ์žˆ๋‹ค.
  • ๋ณธ ์ด์Šˆ๋Š” ๋ณ„๋„ ์ž‘์—…(์ฝ”๋“œ ๋ณ€๊ฒฝ)์„ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ํ˜„์žฌ ์ธ์ฆ ๊ตฌํ˜„์ด ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€๋ฅผ ์ฝ”๋“œ ๊ธฐ์ค€์œผ๋กœ ์ •ํ™•ํžˆ ๋ฌธ์„œํ™”ํ•˜๊ธฐ ์œ„ํ•œ ๋ถ„์„ยท๊ธฐ๋ก์šฉ ์ด์Šˆ๋‹ค. (์ž‘์—… X / ํ˜„ํ™ฉ ๋ถ„์„ ์ „์šฉ)

๐Ÿ”Ž ์ธ์ฆ ์ „์ฒด ํ๋ฆ„ ์š”์•ฝ

[Client] ํ•™๋ฒˆ + ๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ
   โ”‚ POST /api/auth/signin  (์›น)  ๋˜๋Š”  /api/auth/mobile/signin (๋ชจ๋ฐ”์ผ)
   โ–ผ
AuthController
   โ”‚
   โ–ผ
AuthApplicationService.signIn() / signInForMobile()
   โ”‚
   โ–ผ
SejongPortalAuthenticator.getMemberAuthInfos()
   โ”œโ”€ 1. portal.sejong.ac.kr/jsp/login/login_action.jsp  โ†’ id/password POST ๋กœ๊ทธ์ธ
   โ”œโ”€ 2. classic.sejong.ac.kr SSO ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ(sso-return.jsp) โ†’ ์„ธ์…˜ ์ฟ ํ‚ค ํ™•๋ณด
   โ”œโ”€ 3. classic.sejong.ac.kr/classic/reading/status.do (๊ณ ์ „๋…์„œ์ธ์ฆํ˜„ํ™ฉ) GET
   โ””โ”€ 4. Jsoup ์œผ๋กœ "์‚ฌ์šฉ์ž ์ •๋ณด" ํ…Œ์ด๋ธ” HTML ํŒŒ์‹ฑ โ†’ ํ•™๊ณผ/ํ•™๋ฒˆ/์ด๋ฆ„/ํ•™๋…„/๋“ฑ๋ก์ƒํƒœ ์ถ”์ถœ
   โ”‚
   โ–ผ
[Member ์กฐํšŒ or ์‹ ๊ทœ ๋“ฑ๋ก]  โ†’ ์ฒซ ๋กœ๊ทธ์ธ ์‹œ Exp ์ƒ์„ฑ + ์—ฝ์ „ ๋ณด์ƒ ์ง€๊ธ‰ + Faculty ๋งคํ•‘
   โ”‚
   โ–ผ
[JWT ๋ฐœ๊ธ‰]  AccessToken(1h) + RefreshToken(7d)
   โ”œโ”€ ์›น   : RefreshToken ์„ HttpOnly ์ฟ ํ‚ค(Set-Cookie)๋กœ ๋‚ด๋ ค์คŒ, AccessToken ์€ ์‘๋‹ต ๋ฐ”๋””
   โ””โ”€ ๋ชจ๋ฐ”์ผ: AccessToken + RefreshToken ๋ชจ๋‘ JSON ์‘๋‹ต ๋ฐ”๋””๋กœ ๋‚ด๋ ค์คŒ
   โ”‚
   โ–ผ
์ดํ›„ ๋ชจ๋“  ์š”์ฒญ์€ TokenAuthenticationFilter ๊ฐ€ Bearer AccessToken ๊ฒ€์ฆ (STATELESS)

๐Ÿงฉ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ / ํŒŒ์ผ ๊ฒฝ๋กœ

๊ณ„์ธต ํด๋ž˜์Šค ๊ฒฝ๋กœ
Controller AuthController SM-Web/.../web/controller/api/AuthController.java
Application AuthApplicationService SM-Application/.../application/service/AuthApplicationService.java
Domain(์ธ์ฆํ•ต์‹ฌ) SejongPortalAuthenticator SM-Domain-Auth/.../auth/service/SejongPortalAuthenticator.java
Domain(ํ† ํฐ) JwtUtil SM-Domain-Auth/.../auth/util/JwtUtil.java
Domain(ํ•„ํ„ฐ) TokenAuthenticationFilter SM-Domain-Auth/.../auth/filter/TokenAuthenticationFilter.java
Domain(์œ ์ €๋กœ๋“œ) CustomUserDetailsService SM-Domain-Auth/.../auth/service/CustomUserDetailsService.java
Config WebSecurityConfig SM-Web/.../web/config/WebSecurityConfig.java
Common(ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ) SecurityUrls SM-Common/.../dto/SecurityUrls.java
DTO AuthCommand / AuthDto SM-Domain-Auth/.../auth/dto/

๐Ÿ›ฐ๏ธ 1. ์„ธ์ข…ํฌํ„ธ ํฌ๋กค๋ง ์ธ์ฆ (SejongPortalAuthenticator)

  • ์ง„์ž…์ : getMemberAuthInfos(AuthCommand) (L48)
    • sejongPortalId, sejongPortalPassword ๋‘ ๊ฐ’์„ ์ž…๋ ฅ์œผ๋กœ ๋ฐ›์Œ (AuthCommand L26~27).
  • ํ…Œ์ŠคํŠธ ๊ณ„์ • ๋ถ„๊ธฐ (L52~55): ํ•™๋ฒˆ์ด "9"๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์„ธ์ข…ํฌํ„ธ์„ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  getTestInfos()๋กœ ์šฐํšŒ โ†’ test_member ํ…Œ์ด๋ธ”์—์„œ ์กฐํšŒ ํ›„ BCrypt ๋น„๋ฐ€๋ฒˆํ˜ธ ๋งค์นญ(L92~112).
  • ์‹ค์ œ ์ธ์ฆ ๋‹จ๊ณ„:
    1. ํฌํ„ธ ๋กœ๊ทธ์ธ doPortalLogin() (L117~143)
      • POST https://portal.sejong.ac.kr/jsp/login/login_action.jsp
      • form ํŒŒ๋ผ๋ฏธํ„ฐ: mainLogin=N, rtUrl=library.sejong.ac.kr, id, password
      • ํ—ค๋”: Host, Referer, Cookie: chknos=false
    2. SSO ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ (L65~72)
      • GET http://classic.sejong.ac.kr/_custom/sejong/sso/sso-return.jsp?returnUrl=... ๋กœ ์„ธ์…˜(์ฟ ํ‚ค) ํ™•๋ณด
    3. ๋ฐ์ดํ„ฐ ํŽ˜์ด์ง€ ์ˆ˜์ง‘ fetchReadingStatusHtml() (L150~165)
      • GET https://classic.sejong.ac.kr/classic/reading/status.do โ†’ HTML ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
    4. HTML ํŒŒ์‹ฑ parseHTMLAndGetMemberInfo() (L173~198)
      • Jsoup selector: .b-con-box:has(h4.b-h4-tit01:contains(์‚ฌ์šฉ์ž ์ •๋ณด)) table.b-board-table tbody tr
      • ํ…Œ์ด๋ธ” ํ–‰ ์ˆœ์„œ๋กœ ํ•™๊ณผ(0) / ํ•™๋ฒˆ(1) / ์ด๋ฆ„(2) / ํ•™๋…„(3) / ๋“ฑ๋ก์ƒํƒœ(4) ์ถ”์ถœ โ†’ AuthDto ๋นŒ๋“œ
  • HTTP ํด๋ผ์ด์–ธํŠธ buildClient() (L234~263)
    • OkHttpClient ์‚ฌ์šฉ. ์ฟ ํ‚ค ์ž๋™๊ด€๋ฆฌ(JavaNetCookieJar, CookiePolicy.ACCEPT_ALL).
    • โš ๏ธ SSL ๊ฒ€์ฆ ์ „๋ฉด ๋น„ํ™œ์„ฑํ™”: trustAllManager()(๋ชจ๋“  ์ธ์ฆ์„œ ์‹ ๋ขฐ, L268~279) + hostnameVerifier = (h,s) -> true(L242).
  • ์žฌ์‹œ๋„ executeWithRetry() (L211~226): SocketTimeoutException ์‹œ ์ตœ๋Œ€ 3ํšŒ ์žฌ์‹œ๋„, ์‹คํŒจ ์‹œ SEJONG_AUTH_CONNECTION_ERROR.

๐Ÿ‘ค 2. ํšŒ์› ์ฒ˜๋ฆฌ (AuthApplicationService)

  • ์ธ์ฆ ๊ฒฐ๊ณผ(AuthDto)์˜ ํ•™๋ฒˆ์œผ๋กœ memberRepository.findByStudentId() ์กฐํšŒ, ์—†์œผ๋ฉด ์‹ ๊ทœ ๋“ฑ๋ก (signIn L78117 / signInForMobile L242281).
    • ์‹ ๊ทœ ํšŒ์› ๊ธฐ๋ณธ ๊ถŒํ•œ ROLE_USER, AdminProperties.isAdmin(studentId)๋ฉด ROLE_ADMIN ์ถ”๊ฐ€.
    • ์‹ ๊ทœ ํšŒ์›์— Exp ์—”ํ‹ฐํ‹ฐ(ํ‹ฐ์–ด R, exp 0) ์ƒ์„ฑ.
    • ๋‹‰๋„ค์ž„์€ UUID ์•ž 6์ž๋ฆฌ๋กœ ์ž„์‹œ ์ƒ์„ฑ(uuidNickname).
  • ์ฒซ ๋กœ๊ทธ์ธ(isFirstLogin)์ด๋ฉด ์—ฝ์ „ ๋ณด์ƒ ์ง€๊ธ‰(yeopjeonProperties.getCreateAccount()) ํ›„ ํ”Œ๋ž˜๊ทธ ํ•ด์ œ (L141~154).
  • ํšŒ์›์˜ major๋กœ Faculty(๋‹จ๊ณผ๋Œ€) ๋งคํ•‘ (departmentRepository.findDeptMPrintOrDeptSPrint, L124~138). ๋งคํ•‘ ์‹คํŒจ ์‹œ NOT_FOUND ์ฒ˜๋ฆฌ.
  • lastLoginTime ๊ฐฑ์‹  ํ›„ ์ €์žฅ.
  • โš ๏ธ ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์šฐ๋ฆฌ DB์— ์ €์žฅํ•˜์ง€ ์•Š์Œ: ์„ธ์ข…ํฌํ„ธ ์ธ์ฆ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , Member์—๋Š” ํ•™๋ฒˆยท์ด๋ฆ„ยทํ•™๊ณผยทํ•™๋…„ยท์ƒํƒœ๋งŒ ์ €์žฅ.

๐Ÿ”‘ 3. JWT ํ† ํฐ (JwtUtil)

  • ์„ค์ •๊ฐ’(@Value): jwt.secret-key, jwt.access-exp-time(=3600000ms / 1h), jwt.refresh-exp-time(=604800000ms / 7d), jwt.issuer(=SEJONG_MALSAMI).
  • ํ† ํฐ ์ƒ์„ฑ createToken() (L104~115): subject=memberId, claim์œผ๋กœ category(access|refresh), username(memberId), issuer, iat, exp ํฌํ•จ. ์„œ๋ช…์€ HMAC-SHA (Base64 ๋””์ฝ”๋”ฉํ•œ secret-key โ†’ Keys.hmacShaKeyFor).
  • validateToken() (L123~144): ๋งŒ๋ฃŒ(ExpiredJwtException)๋Š” ํ˜ธ์ถœ๋ถ€๋กœ ์žฌ๋˜์ง€๊ณ , ๊ทธ ์™ธ(์ง€์›์•ˆํ•จ/ํ˜•์‹์˜ค๋ฅ˜/์„œ๋ช…์˜ค๋ฅ˜/๋นˆ๊ฐ’)๋Š” false ๋ฐ˜ํ™˜.
  • RefreshToken ์ €์žฅ: MongoDB RefreshToken(token, memberId, expiryDate) ์ปฌ๋ ‰์…˜์— ์ €์žฅ.

ํ† ํฐ ์ „๋‹ฌ ๋ฐฉ์‹ (์›น vs ๋ชจ๋ฐ”์ผ)

๊ตฌ๋ถ„ AccessToken RefreshToken
์›น (/signin) ์‘๋‹ต ๋ฐ”๋””(AuthDto.accessToken) HttpOnly ์ฟ ํ‚ค (Set-Cookie, SameSite=None; Secure; HttpOnly)
๋ชจ๋ฐ”์ผ (/mobile/signin) ์‘๋‹ต ๋ฐ”๋”” ์‘๋‹ต ๋ฐ”๋””(JSON, ์ฟ ํ‚ค ๋ฏธ์‚ฌ์šฉ)
  • ํ† ํฐ ๊ฐฑ์‹ : ์›น refreshAccessToken()(์ฟ ํ‚ค์—์„œ RefreshToken ์ถ”์ถœ โ†’ Access ์žฌ๋ฐœ๊ธ‰), ๋ชจ๋ฐ”์ผ refreshTokensForMobile()(Access+Refresh ๋ชจ๋‘ ์žฌ๋ฐœ๊ธ‰, ๊ธฐ์กด Refresh ์‚ญ์ œ ํ›„ ์‹ ๊ทœ ์ €์žฅ).

๐Ÿ›ก๏ธ 4. Spring Security / ํ•„ํ„ฐ (WebSecurityConfig + TokenAuthenticationFilter)

  • ์„ธ์…˜ STATELESS, csrf/httpBasic/formLogin ๋น„ํ™œ์„ฑํ™” (WebSecurityConfig L56~111).
  • ์ธ๊ฐ€ ๊ทœ์น™: SecurityUrls.AUTH_WHITELIST permitAll, SecurityUrls.ADMIN_PATHS ๋Š” ROLE_ADMIN, ๊ทธ ์™ธ ์ธ์ฆ ํ•„์š”.
    • ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ ์˜ˆ: /api/auth/signin, /api/auth/mobile/signin, /api/auth/refresh, Swagger(/docs/**, /v3/api-docs/**), ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋กœ๊ทธ์ธ(/login) ๋“ฑ (SecurityUrls L14~).
  • CORS: ํ—ˆ์šฉ Origin 6์ข…(์‹œ๋†€๋กœ์ง€ ์™ธ๋ถ€ ๋„๋ฉ”์ธ 2 + localhost 4), allowCredentials=true (L4249, L132144).
  • TokenAuthenticationFilter (addFilterBefore, UsernamePasswordAuthenticationFilter ์•ž):
    • ํ™”์ดํŠธ๋ฆฌ์ŠคํŠธ๋ฉด ํ†ต๊ณผ(L44~47).
    • /api/**: Authorization: Bearer <token>์—์„œ ์ถ”์ถœ(L57~62).
    • /admin/**: Authorization ํ—ค๋” ์šฐ์„ , ์—†์œผ๋ฉด accessToken ํŒŒ๋ผ๋ฏธํ„ฐ(L63~74).
    • ๊ฒ€์ฆ ์„ฑ๊ณต ์‹œ CustomUserDetailsService.loadUserByUsername(memberId) ๋กœ๋“œ โ†’ SecurityContext ์ธ์ฆ ์„ค์ •(L78~82).
    • ์‹คํŒจ ์‹œ API๋Š” JSON ์—๋Ÿฌ(MISSING_AUTH_TOKEN / INVALID_ACCESS_TOKEN / EXPIRED_ACCESS_TOKEN), ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋Š” /login ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ.
  • CustomUserDetailsService (L22~38): username = memberId(UUID) ๊ธฐ์ค€์œผ๋กœ Member ์กฐํšŒ โ†’ CustomUserDetails ๋ฐ˜ํ™˜. ๊ถŒํ•œ์€ Member.roles ๊ธฐ๋ฐ˜.

๐Ÿงช 5. ํ…Œ์ŠคํŠธ ๊ณ„์ • ์šฐํšŒ (ํ•™๋ฒˆ 9...)

  • ํ•™๋ฒˆ์ด 9๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์„ธ์ข…ํฌํ„ธ์„ ํ˜ธ์ถœํ•˜์ง€ ์•Š๊ณ  PostgreSQL test_member์—์„œ ์กฐํšŒ(SejongPortalAuthenticator.getTestInfos L92~112).
  • ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” BCrypt(PasswordEncoder.matches)๋กœ ๋งค์นญ.
  • ์•ฑ ์‹ฌ์‚ฌยท๋กœ์ปฌ/QA ํ…Œ์ŠคํŠธ ๋“ฑ ์‹ค์ œ ์„ธ์ข…ํฌํ„ธ ์ธ์ฆ์ด ์–ด๋ ค์šด ํ™˜๊ฒฝ์„ ์œ„ํ•œ ์žฅ์น˜๋กœ ๋ณด์ž„.

๐Ÿ“Œ ์ฐธ๊ณ : ๋ถ„์„์—์„œ ๊ด€์ฐฐ๋œ ์ž ์žฌ ๋ฆฌ์Šคํฌ (์ž‘์—… ์•„๋‹˜, ๊ธฐ๋ก์šฉ)

  • ์™ธ๋ถ€ HTML ๊ตฌ์กฐ ๊ฒฐํ•ฉ: ์„ธ์ข…ํฌํ„ธ ํŽ˜์ด์ง€ ๊ตฌ์กฐ/์—”๋“œํฌ์ธํŠธ ๋ณ€๊ฒฝ ์‹œ ๋กœ๊ทธ์ธ ์ „์ฒด๊ฐ€ ์ฆ‰์‹œ ์‹คํŒจ ๊ฐ€๋Šฅ (Jsoup selector ์˜์กด).
  • SSL ๊ฒ€์ฆ ์ „๋ฉด ๋น„ํ™œ์„ฑํ™”(trustAllManager + hostnameVerifier true): MITM ๊ด€์ ์—์„œ ์œ„ํ—˜. (์„ธ์ข…ํฌํ„ธ ์ธ์ฆ์„œ ์ด์Šˆ ํšŒํ”ผ ๋ชฉ์ ์œผ๋กœ ์ถ”์ •)
  • ์ฟ ํ‚ค Secure/SameSite ๋ถ„๊ธฐ: ์ฝ”๋“œ์ƒ setSecure(false) + ์ˆ˜๋™ Set-Cookie์—์„œ๋Š” Secure; SameSite=None ์„ ๋ถ™์ด๋Š” ๋“ฑ ํ™˜๊ฒฝ๋ณ„ ๋ถ„๊ธฐ์— FIXME ์ฃผ์„ ๋‹ค์ˆ˜ ์กด์žฌ.
  • ๋ฏผ๊ฐ์ •๋ณด ๋กœ๊น…: AuthApplicationService์— accessToken/refreshToken ๊ฐ’์„ log.info๋กœ ์ถœ๋ ฅํ•˜๋Š” ์ฝ”๋“œ ์กด์žฌ(L168169, L332333 ๋“ฑ).

์œ„ ๋ฆฌ์Šคํฌ๋“ค์€ ๋ณธ ์ด์Šˆ์—์„œ ์ˆ˜์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ถ”ํ›„ ๊ฐœ์„ ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ฐ ํ•ญ๋ชฉ๋ณ„๋กœ ๋ณ„๋„ ์ด์Šˆ๋ฅผ ๋ถ„๋ฆฌํ•ด ๋‹ค๋ฃฌ๋‹ค.


๐Ÿ› ๏ธ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ / ์ œ์•ˆ

  • ๋ณธ ์ด์Šˆ๋Š” ํ˜„ํ™ฉ ๋ถ„์„ยท๋ฌธ์„œํ™” ์ „์šฉ์ด๋ฉฐ ๋ณ„๋„ ์ฝ”๋“œ ์ž‘์—…์„ ๋™๋ฐ˜ํ•˜์ง€ ์•Š๋Š”๋‹ค.
  • ๋ณธ๋ฌธ ๋‚ด์šฉ์€ ์ถ”ํ›„ ์ธ์ฆ ๋ฆฌํŒฉํ† ๋งยท๋ณด์•ˆ ๊ฐœ์„ ยท์„ธ์ข…ํฌํ„ธ ๋ณ€๊ฒฝ ๋Œ€์‘ ๋…ผ์˜ ์‹œ ๊ณตํ†ต ๋ ˆํผ๋Ÿฐ์Šค๋กœ ํ™œ์šฉํ•œ๋‹ค.

๐Ÿ™‹โ€โ™‚๏ธ ๋‹ด๋‹น์ž

  • ๋ฐฑ์—”๋“œ: -
  • ํ”„๋ก ํŠธ์—”๋“œ: -
  • ๋””์ž์ธ: -

Metadata

Metadata

Assignees

No one assigned

    Labels

    ์ž‘์—…์ „์ž‘์—… ์‹œ์ž‘ ์ „ ์ค€๋น„ ์ƒํƒœ

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions