From 7a8f5cb3cce64d3df64eb790cbf7e8688084a800 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 14:55:55 +0900 Subject: [PATCH 01/36] chore(assets): remove unused files --- .../\355\224\204\353\241\234\355\225\20401.png" | Bin 7522 -> 0 bytes .../\355\224\204\353\241\234\355\225\20402.png" | Bin 8370 -> 0 bytes .../\355\224\204\353\241\234\355\225\20403.png" | Bin 8101 -> 0 bytes .../\355\224\204\353\241\234\355\225\20404.png" | Bin 7393 -> 0 bytes .../\355\224\204\353\241\234\355\225\20405.png" | Bin 8522 -> 0 bytes .../\355\224\204\353\241\234\355\225\20406.png" | Bin 8214 -> 0 bytes .../\355\224\204\353\241\234\355\225\20407.png" | Bin 7178 -> 0 bytes .../\355\224\204\353\241\234\355\225\20408.png" | Bin 6884 -> 0 bytes .../\355\224\204\353\241\234\355\225\20409.png" | Bin 7698 -> 0 bytes .../\355\224\204\353\241\234\355\225\20410.png" | Bin 8236 -> 0 bytes .../\355\224\204\353\241\234\355\225\20411.png" | Bin 8187 -> 0 bytes .../\355\224\204\353\241\234\355\225\20412.png" | Bin 3811 -> 0 bytes .../\355\224\204\353\241\234\355\225\20413.png" | Bin 8357 -> 0 bytes .../\355\224\204\353\241\234\355\225\20414.png" | Bin 7992 -> 0 bytes .../\355\224\204\353\241\234\355\225\20415.png" | Bin 8510 -> 0 bytes .../\355\224\204\353\241\234\355\225\20416.png" | Bin 7681 -> 0 bytes 16 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20401.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20402.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20403.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20404.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20405.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20406.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20407.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20408.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20409.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20410.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20411.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20412.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20413.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20414.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20415.png" delete mode 100644 "service/app/public/profiles/\355\224\204\353\241\234\355\225\20416.png" diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20401.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20401.png" deleted file mode 100644 index 79e7cef0a6fb5e1cc3b7e1ea73cfbf3e6a0a251a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7522 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 RG+`tPP*c`cs(xV=_J3s8iu(Wn diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20402.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20402.png" deleted file mode 100644 index eaf888c0a321cfa009f6282898b4d493b6feaa49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8370 zcmYjXc|4R~)F1m&_N45DVPr`pB8g$_gF*I$jLE)cUrQ=k2HCQXZEUHruay$AjTy=^ zO18m_5+kPJo!|R;|9JnnpXYPF_uTI}_dfTz_uO-zbf~2<7pE{M007`Jff(8V01W?I z*jedFIo@HH{@@6LIEDfM=VkvZ3??=*zvv)CsEzS$K=XtskuEU%=v(Lm0Iiv4{&}zf z0FuTghWfCF45TIZP#18~#&4jN31iCX=f74n7RJ!~RqvQZQql|hjRIJukMA)ZjuY;= zG3+Z2#BZOnqNQ8=0lR?G3oi?Ui(6Uv^A|rsHdaeEV0kV% zXjm4g?ygH|OCNJ2_vqltkA*PW`0;?EWjDqlP%FP1cx=y}ev6!7Uk zUBpx_&gZm0uC~wewL5CO6RONLN+E1b3lSv}57glf>hPwolNpAJ>Ddz^leNoitA<(k z-FJoPc=9Csv#@}lk<=*vIPR2-u)tCBx z6vFE5x6}x3uq3|~6>cOZTy_?Tn|I3cZf7h?lH~fj?A~FP{kdUwUUqr_s_U6=aZfL! z3SNlNaAEen`{oyBq?o9%4@O&d?4$7w>FGD)XhQcLdCw-R15#@fj-3Imh7vaQ$C;7` zu!uWes}?3XksMY7-W7!eT$bQNiH~qU7nc+A_ab;rUWInof<}g0boC;^ z-?@@;OqZ96YX|l-S2oq`&{TFCW9ztkY^8$*4Bbk*uMgAvy6wQ(Xb9) z)Qe4}<>fcTJ=er5&$AUTQ)A4!kP4)1Q^eOrJ>v4Ec@pGa3MP` zninM~b<0|ovCWCucceM);AhvJbpr5~H5+4_Fte{ox99e%?gL~bOI_iWX*TQ268v>* zl8`e-67nwiU4!b@frre#NpIYyWR9NrXn$AZ!pS@(+|P9nX2&wRP=O0CoGF(m`ac9H zR?iHwCKA1MVqeW$xi+@1t-vR z8)p5=^2yV%vuJ2)fR#qK5Z;#?hGHw{1aCfuRmE7pTE7L>Hk=g_{$^#kqs71&LBV{k zIEpQMFm$;%KdN{uOb>+BJ^4{aayH40HF_c)mL{~9R`=%KOYGME{8k_7OtNboB<6xz za!nLD^>gd6Hz+$@Su6qTa^8#G#zP@<-Lb%or!Zdnsz?1P(HbryjK|Hv(o5 zu6cq8dvZ8OUhuS7()3>xSD;qF-$+x8c(Ay*86zdq0Sy(Vw=XR9v_?A^Vv1kZbd9;Y zfR)VPz&7gP+B>ULjbgS;>Tmb>ra?2+SfP~1zG8B+ESO}1QUivbf8fh-DdZYX1)ccpMl5vtVGKgWZpBz z@_a-XbkjrFg^GL(|5vrL4B0-M<&yxE~DrLj#Y zRxfey8id38F6K=dGIwx$CRQZ{7I>Mjau6=a1yq;^ySE^tg;2+{I-2>3(_6jV+6@FQ z`Q7)p2FY>OtY4$RAjx6jU%(oVt%Vs`0<(f`N#gI=W~0N0%e8#5Zi{WFX(}Bmr~#Z6 z^I(!&w041{-FEBHhLwcd9ll{L_iV(6K0blPC6{veFgJ@}C8AZf$tw0?`w&r5tg~&% zE9UO=nfOv%CQ{dV<(LHYNl`CW7{yyj3W?=yd=JX0#JCR7R{gT%ocF|~I9EfZzaYZW z2=O6ZN~0r^)#DtWCu_IM-+qoZ7t>fi=xK%}Wb80^SqY~N_shhQzp9`X<_WQzS^}tV{ql4Z>zh}l^ENXGq3fa_ z({-_@e-0=g+t}2Xn2=MledNq*j?sYd6I}L@cfBJ?AKh^t^CRL<1&01ne*0$)Smpg1 zKII!;*|eGWb9lTPS4XL)%^w@+Rs?SIgO>@j%c5KhI&5<1r-E`EwOZYs9%La>NUfLt zklyXiEQ@x#NI{dPV&kX8y~w1`N~n^%2ytvN8=eG+@pzxnDLMdtJg>(&f0WL*f@ z^Uk!qGM!PvQcXZHqP|%+e$9;9#?_NsaIZOWE98`7)ctSyd^w+Rgjkqx+vVSiO^~;; zeF|jGx|7GzL?EmV{+QK`?Uj*|*+0ZfitT0Mp9^PUuEzi=#Af(ZI=*$N#m~3wFt?w* zn5PFwQ$M_P&M5=Ynz#sZU2(9_Ue~QwVH~HibKy6K{zLdteIv8}^%v++l6}?jYcaVh zvw!%QW5(bkQ}=lvvt!cg+kGE&T)gBs?bmeu2WdS$xq^vkjNmH;s&YJ0(6<|vF!_kr zPW|wMaV^i8nvQ}mX2d|+3^kewcy&?VqoAp03)vUyh z;?`1Z$44f5OYrki#m9Va|KWaY_U586)wHd33ivmCMR%MQ#8tKTw2I9tQnNiDB~uMU z(LBaie$PlbRS--Racr=q?sv^EWudB_Z_Ql1Dj5zM`+@% z)@@7>DNu98%qIE0A{G>=YL`B%08^kcPUP0ALAkB3!{w4~VIy;5k# zIgsWK2MZ%Kw9g8HM&H-P&iK_aJ5VD?2_@|x zt)V@7_fhl{PC-wd15~-wBfILW4k%dF(UmTVD%BFywMVf32mH&GjdSI!H}ye7>WzF1 zR6ytOaMJze|C2xXE6Ag#=f_?0EM2oH=;_IQAu0J(`?|;(+mnN|l6}ob+`ws)I4*!T z^kWvcom{Voz5e25Dz8$mecj~c2sZmo5#9aD$C(J+#b*tNBplD0g=_Y)I*s5%N1t_U zEvb#2qt%0KmTQ)&&lhZdtTaV;d{F}@pd~Y8qRpTRj2f3+*urg#{B4zXfo}6Nd#kKP zRNAK?ejf`wr$#W?bPOqSJWlhm`oo6RgjHEOo3A6Esa5#qQ_L8BM2l*L@S430>+Snf zM2o^HL;X=HWDlHtm=^y#rKd(KtVkYmNl^f(fo{`;Y88Y`3NY9Ik<$$ujr%$zg)G}| z#~tz*k{9VDg1x+3kJ7ncg*c%4ip_sLx38<^p3z>*NsHD0^KON*Na^Y=E4cxg;rbfM z>{ca&=fX4|Rd_pf-N+45B00{crEk|Bz`;wpJN6Zs6;;e#FAuvwSm)(Mm3^X3XN7yk z{g?>MDooC@`=!G7-bLN&u6{YSa@0j}zv?ub;W2;n19#(sfv&%QvyA!vj@4H4qVMT>S1&8av6|Gr-<(HWyMrR2^;pT*vyk>ig)9xx<1opPd)iVti<(8_riY=Q4@bALo8JBN_LNQ`5kDAU}tr=;TB#O#$EFi&jaJa&ZPCk)JwfiUrP8oBtp3r8Mxa+C6=YJF(Vuq zh1L5)H*TJ>5$|E5RE48mq?i%HD96VUR-}%Z6gfbTDxAJ2FXm&VVS4xVg%=8F)_0j1 z+gwtTwiZ@TNF-8w{FiuV6p*r?#pnqy-*VT!NSu%xr-8Sv$aA_|@z093n{0g}+7CQW zw-p5yvXB#7xPgL2_ZK^ZlSnt%CAqPQ05FK+Xr_5nm9L08wh>!CPd>8gkk**4oO;e< z{%HMVPitXOV&n=fi={q~JR*=tByr((6E+F@#=)$`)bN7Hr6fe7@3t`ol9YVFlt*u{ zNuRW72ZxpOZV~rIsumH9qu|hlF2q^fP6X1U{3=w8-hpstG$BCEg@*-gT%O-BIzn5L z^j5nG6`}i3E#(vuI$$}v@m>Uai7wmOh3e^ZUQeVm=UuF`_WRim2)aEGzExd&G0P_Z zT}O48tg51z3qJ>7172RLD;olLgYt9Xwi}iP?sLJC&jsb)4F0#6PBzX3J|Mfl@>d3( z3NdicjPB!*$(&!*;M!{0KceNotH!CmKx4F#tmp}ujG>T8@u0%TaCw!$V=qJATaM4QtJ|}LnQRp>`1m1^oXKXGl!C0UzY|buh z2hT{G3HSAjWJW9)pglK=88h!FIz~h&+I&b7fND0UM1STMzjl$kII;Um$&lVBLor$b zOT9(+b}ZnaT=)PvA&t*;%xbShT>>BsG`4EHhBxf@JtKVwG?w`QZFhs-cFZ0`*Tr8t zgC&sUxJwd+{iYIk>y7=ZQn-%97@%6sDaRx8bn%9Mk@)pEWdP`75Pw3UhhY7c#Q;XM zk;ayaz_+}U4hcI{{(1RwQ&?5J*@|&tmjJ3s-jNTGR}qK{q|E{kHeaXHtJUVohGWRN z2&XcfWfsM$T$`~xte1OB%QK?sd*t{f3oCMh3c!>ehXHnoA@hPLIkg=mqwG&@IKUjE z^S2{L5&Hrl&xMCy$k7$}6T#xE7@%|}5ev;=^`K>J@O-_rHpn5*^TCACgA$lTj`{b3 z30N+A7RWJ_wY^VG&izkvVdEMqO_6I;fe4Io0n6cZ*;q0Aw;9#_ebqEqliZo&$&kkbxEWAw75w1of}R6nHpW<(wATP@$I@gM8G7U zW$+7TiTJKvk~jP8ZkvQKDs&-~?mkPWg@*8RYibn&o`vwdNkt9%!3TN1JfT?&W*jpp zQfwJiHJVc%f1HPV$r)88NV=T|*m_(ydFXV;7KR$;QF(2}X`YTySN(LE$8u;{QTvxf z`(7)cfE^Ei=oj+Kej`c02ohokFi-U{eD7IBJKSayyMV&zGa#wteo4uB3MTSo(~XT) z!5=#4)yXYf&NN?oTp3)JCBs2Q-Lj;Qjun6ACQ zhWpi282zo|Qq#=i1nBIc!JDb9o-nsgIbE;l{Yi@?1EBgvJiMSr%WJC`1QFDHHDw5^ z`tfuHzGYsl4QT^e=!G%Ed5?{ls#{Vm9W&5cg&72JoO>ec)H@kFZs0daLi6OnmA#D^ zBXP4+lz7yaQI)b*qH*y1rm$x}$9l3GYusJ-=OR})6jk&{6D@(#v#b3MdS8Y}Rw?jD zw~>o34b9TDmF8@{SpU@@loJUmJ~X{VSGMhVIN|DEW(Q?B?eS(ynbqp5z)FU%b1ADu zAI722zOFDw^p}3#oPHZWt?X1y#}F?OVDxPR2R4Ym^8`c-r!6^5e|GOd*7!vUtJ`MX zKTRfWGI@Z`F*>XT{YBk{;=+MOu)0Q@(C= z8q)8d>dn-XUTb&g(&!b>y9fP$D-sc~aZ%K_fmHN4hK6Vu;e*WZ#$QZW1k4&=2a@@( zAzHhXFRC$1FK_MP1sfiJeLB8#s)n{ng;zbE(-&Ib`8ql6?jUC8OOWl^R)T`)p&?~3F%ytU5ZRWzKY_(!kx333vXNQKByNL(c zP1C8>|1+7>{TR8)RiSh8uxDNpPf8Vea1?J@E3$W{*~sq4!h9bZb189nOVl!CZ-ksT z7QFv`!~T&+!nRvKZ`?Y(-#tNWw2rQ*kG8ANTM>|`!!8vqduqQeXaUhaj3dxCO-84O zO|g1&g+Iy4+nxfaMy62Hs*fWdLLCYmINg1FW>Si%>h9O`RG! zRKUkOlsQPyU!VQcO8Jf&%H|jC>cw?vq|*$}CUXtLa85fnr}SA}Lf{46yG1dvWF`gc zdj05s&||^G2>FMVTR&ue>df*VZI`XJ{8Bg9qt(LKb#42uRNHOJNTI{H>yi91NB=I` zBJ>?bvX|*+W$uK4Gv8l>*fgzHW=_k8W{`1;CiT`qTyvnRFi>b({uvdyELzO^UW* z-7p3`ulJXm78nxxg+?;M3y(KCf8ASIm9kY#pUCafoOk$!e}d0w;(4A<;2YHM&|Hka z@7QvGYxPK4l*B^lW8|(?FF4&!h`4HakVrfb+E6iJ0iCfSL?(}6gu`itYu39mc%s9k zMSC1HPnzISWay3Lc`IU-9_I`g{<~*SQ%NAifpLP(kL#Z1Z1%$HuzYWTnG>!WpIih7 zrIb~Ohapnt;aa;PPx6Ag8Ut2Y#dmv&L60}lTB^Uf;?a_U=Ea>A8%dVUTyl}z}T0K0G{ zB3t~)@l;)AcjscFo}&Nj)4k@AQt3i(qt4ueOdVC?!A3*J_O7 zw`8t=u}~$;X&2-V6R)XlvJ59b;|`IAsjIT0b%r@)BiNCJER3rPq) zk(1~6+yChdCYi?uU*=OpE@Y+IwWik@iRFFGbUstVNB2+HPuM%$bDPPtGLl)pv&$%} zTMlJJ?G?7p=GKn#I&qz^dS8y;Jk(@qQZ?JB~^36Q8c$zd8ClR}ur21&cKCu1c0hA9|r|1+HQKz@Zt zLdlpAxR1oDL3}6{>{eJz3{`rJwh|{?$#4QPbZ_O<=0-QynU|1qwmB(v8pxD(7wcoQDgRf z#E$znEAeFUucay%BPN8|mx~?u`+}vKmTvmXA-8za$1cTWQ$Cc2G&C*dwd~$UF?}~! zdD|t3*sXH%r|_u~cBl_8ihJ7jB;U8s=cBTTL9m!Q8zWAOxjv<6G0jv%TQp-ofUcg; zf)G{F@cw3{p>60CS2Pqatb5*7>edoCEBf|J+NYvtQV#X7Dp#jCr{Y+@CKvA`GFvk4 zDh;HOv-v7aMWZ0y)+xhnVBr^QEZQoNeycflJ3NN!>RGAz@lSPvXG=;1&x`m;LhUj; z_SV_UG9FGW#aJfeLo<7S1-@ynHHRok0OvExd79{;rVu zofnlP^3^tO41a>-ZJ6E}{9GVN(J4-n^uI8N#+Rd~F%V_BWw zMqD90c{%j8x;q&^_d|yE6!|?>8q#Wz5*`yub@8eU{Z!!4G@jf>k3#@>$%7ORFTGv+a6&^i#`oyppxC&E&?C z@Ix&c`D3q?^F}z7%WQfuH_SKSXR%QwW*+mZ3Dvgupj7tLId+?KWD3(l=>j3(!Bo*_ z%nT^PS~_a>iQ||4SFGfL_&}lEfb$*xQ#{7Bh?dG7ujTfTtEPU#MsG?BPy)k$wo_%F bQY6fXuIJDhS|apc3;-rzOT*^dca#4IcLiJ> diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20403.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20403.png" deleted file mode 100644 index 9c2b10ffb87b01832d102da679785869f5cbb487..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8101 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 diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20404.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20404.png" deleted file mode 100644 index 26892e4921d2f6e2de8128e3d6da42ac7d94fa52..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7393 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 aVIDOTTQw)EN7E~kz-@gKJ%Wx)!v6q(?KRT? diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20405.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20405.png" deleted file mode 100644 index fd71efa697fe821ffbea3a835ba34406cfac4dd8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8522 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 diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20406.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20406.png" deleted file mode 100644 index e04287e7e95c9a6e0f824c1539689233d5e4c483..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8214 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 diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20410.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20410.png" deleted file mode 100644 index a4e66b95f45264e217bffddd15729a2cddb9d205..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8236 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 A-2eap diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20414.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20414.png" deleted file mode 100644 index 76b969d0b591b3d184dd63c124327f24e162bee3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7992 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
$_ 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 E0D>l==Kufz diff --git "a/service/app/public/profiles/\355\224\204\353\241\234\355\225\20416.png" "b/service/app/public/profiles/\355\224\204\353\241\234\355\225\20416.png" deleted file mode 100644 index a2d44c23d7cf8fc23f24e7b732fe64c6c52404cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7681 zcmXXrby$<%_oG`{l#q#tG$@TQ896{|Fq8&qkWfOx(I6=U3F#UGM#}(c1WBbvC?l1Q z(IxrY=llHrxX=5XbI)_?J?Gwg&WX|2)u5whrv?B3bea%VC;&k8zePz-z~oQ9*d=_Z zJRv4t0Kn~s|1Ba-DDNHtN#q69PzF>Db8Zj-Qb({37yzhEq`9;q0|2reERmi9Mf4I3IVQ4$OSmYsQKnPeR1t94*_j>Am@DRg zd-HQ=cz8bJUl1TeC^^ke`+Pq;aHq|d^iDF(2~=~Ts_J%cLfOYHSKIBcVh##}9Uc|J zl9py>FWK*wiN*+v;U5R4kwOrRKy|k5$o}X}9D0eeFMHuDzAN zAvV)O4;%*ATX=1LXBIE6S?<9PYo)&A6IFZ*yjh<+B>o-(RQttlBVg%+VoU-1%RZPA zqLT;OvXPcYx#?4lXio-?(9bh{S-%aAZNOYr8RO&k+kmq8`TRm5if@L=@iaoFwd^RG zUKtPP&f9T64uv`L?}o8#VX)?A99jDn8MV0z2IWucxHIR(ZF*cJtw+v-Y;p%#V}V1p z$BReX<6zmJq`QJN&Cqi5;#cqG%vh_HYgU=TnG$QpVySKUIE*yy&=;U_f;DSxH48O# z+Ln)vG(!X7U@m(H{Shw&8AZoUr#IW^4@Rm~0AIovTAwIBF=4c00%wBRbFVi~uB4r& zBhlk`d)FJvP3z@hvp%MXyQI$Q47nT71y;{Y{UP!&$sWp)*G>^G`jxr)S9*`aBqP18 z?c(L`T&|{?9R9rRb`vgmj9JTIyJJ*L0nTKyIo?R*VsX92nA>DP)t>WNP_wpU5k*F2?mQ5#@3=>}&$c3yilk*KP792%HZ4W1nO*X*38H|<(YrH1 zrM4?l{$_A~b10Cj5I3at=X@=Q=zNsY=wtCM3a~tBJwU2YbsI$V%4i1k(Cd_yFOix_ z1;H0h&7xVbqIVmd8G>VQzF>64-zQ>H&F2dTasg3$o@Al&FG)8-vaMP$3(<1wAzdP? z-UZh55{OtfFuQ~<&;5t1vI4B!VUm0m;XH0mr%Nh`-bLl(1MtG2u$uxFLNa-{tcf0< zXGFrk)*O6$K3RT`8RuuG#86t7+jvQwn7wlkj0^9T1Ubbyko7a*uI3q#7U*Y@hiX3# z=abxMi<%dycrJ8~4{8^MPVOqR$ki;IuEe`9F?*}3v4>Vgo=4W_Q%gw@FUL!*`i->& zrPPm4QDSd>?k9PE^7KB$tkg>Upin%CVrs)KFRQqL2a;u-Pl_oXNt9Re31IF<>D=Kb z;!5>lC?@*=R8=$85Tf@^H13E3ruG`O2gC}ptVhoC`&=pImYg@yLvsdSR4R%B0@6;) zf~tN&d=yg{$j(dV%V_rnlT7b*#IJpvD{_u^|0a~*RBx|^Nd!z4>p^D2$)bpgD5Vk)KPv{^xh zZQcC!0v!oj4h_g(uu>`=#6K=E&t85iSO2S8{u^c|9?;1rs(X-A4mmDQ zSfgSN0O(9U`lm~mmrVnW(gyJS-o@?>lmRO%`+n4Z23;G`nw3R%SieEqKoC5w9pdg1T+U5mQ_1W5z9L8(eT-S?T@@EKSVNNmFy zA=ytM#7vct|G~cPV3@oR6+{@aHBux-1>pUD$4fjW;XDR&LNFw%^a(8cJ(Qe3DOQrw zK$MqATzKWsmF;u@K-bE%5}*L+mR@fwCaH!r-J|cUi44^K)l2THGNBZH^k3GKKL?nL z_kSDj3^NM;*1jie52&hHnnd62FXkhmfpUo(BoKSX&B;VF>yc1l1OByI0XiRtf1aV` zg-q{|cG|3bEyw;2PjO)h+4@_hWlN6*i>wkW#Sb1U?$k#6sDRj{Ng#2u9v@=sdF&4r zLolNjYHwi0wPeol{RUwGg6R!wa4ng@B!37drd%+=KH;cq@InGsy@vuQ4SugRX4h6@ zk5~s2g|MI(kXC7+BQE4N2N}ZmN>t~B3z_|j7(oQj@!b@Hl&KThsXc=^TY)z(qUsF* zCI&ml?O2dm;M38ye1K9i+O=)2VAczL>d|3pk#J~nTJfkO);c37Mq|! z4aEfj>qOSm!b_b1j*?x~k94S}W_>hmlVOqhnJ zNX!)KZPgKCcwOHHIxMR)%kNi$rKEj?%!*f+%6Y)EKtg zbjCXeV~=28ss&L`Yy^$P@FWU`gBsm6{0}d1l}Tx2O6zq!EiXCL5-QVVI?A09o9Fy` z)MW03l^QB_K3EjEnde?hLTGbLm+U0}l$6;cH+JMUH<^2;=vx++MqFzi#vef*BLq;U z&8X=?#m_P!>*B;dnmr~J0Ah}vY{t2U%)-2NdMOR^bxV#r9eO{vaddP%goHaQf~hCw zHt;O@G1%VF@id_Q1CTzmdzV-ZD0Wr)R(2+B)@3pP@w8NZY&a^^@Y7T-m^q&kidpiP z`K9SWg*{Ku0}y{Ai0J#g%DR#HIT=8?Z~YWgx4$u%Wg3VJ$Pb;mho61}|5DoCECrFl zClDXR$Fk%YmDdMqO2d%Pw~2b3`BZJLTr?NlsiD=p0J{muvIoCk0$~ZdRrY5B4AwIO!&sbN^;AVJgy zLZy$qna2aYl@Y{T*=8-mN>6PN^Q&&QF7%juyyDu0qiz^ONfzhxiewYBRpq_%q zdOgCI5(*^dl6~y0EJP~6FqS1mLoCTmzM<>EVdWBft5j=D>kcyST;^)?bB z6?IYza8IQIxvL?FEzU<$54VMSL@1&8+U$jxv)`pb+-1U)A^r0v@Vsx#Qv?ka1AP8j zn7)tLJWVof#}Hd!XAZ5}_N#!qKa+p?-3K(IG&D@ahMtg6c~-p> zLRkBK0$+OEU#orpO{aS%X4Q~By9^-NdA>?XaLcuJu6(8n7!kxt$QC?B8toQR$x2c+ z*w-yreCY1T=bmG{UfO`-s7#k+@Oz=%N{TXqlU_=8rAUouJ5?l2S!OkoDhYRqw}D0^ zpHalWc$v>bM$XHrByy7sN8NR->?q(_X;llN%a|jEqFV9EiPgAx3!B0X(!Mr zF1R=V=Af?qzoX!zqQl5gthdpnKase&Y0~vY!n^LF3!{>0>v$?JuWr1a@G?MCwj)3; zd(&$?=zOD$58iSQ`FfBddAwBjW`8`7xzyRg?dy#5I2?H$27u6pX1Y&JMIFso?%Ih7 zh|_YAz#6q{qcWL}rLccRc9~6Th^=2rD*3de1ZDBZ|1f%^%on%xWjvm0*4FL5`K`8x zkmG5c*P+C-`6Ch3c{DyMUHVtA_eGn!X9PZdbN%|~daR|<7zsugQxkVeZ55c7d_xyB zk8{v0e9SC-&sS=CERsC-`yA zgj6%hwPKyJ4&i0U-&nplN$b|0U)39xIPIE4b}DUjuJ60$eJW`fVJLLQ*o-1$ML-*` zN0-t`mA3D3Hp#T6r$~*m%!oTdE$c?8rXWJZKx5NL7qav91_5Qs6_f^_5J$(t(YsH~ zx=O7_#+MozRHN~wuY)wkV~zA=-^d6sB=&_-AC-R#;J=^AIOX2-aK!a+gOzAhf=<_M zg`09pHR(P|fBLd24eU}GYz#P3{iTvOzfZ~hk5-VS3MSoAZI3YS$-WJ+p!uNn)IpXx z4;-*G-kWenCla3G1I-VNaB48=@Gse^ze=xws{vG%qaE_x#>yJ$q2#(syb(^I*YnSC zqtXx!`wSl_Fs#}wIGSOoU8|fPLx(+Yx`+ihX`aSx<`nd|GFO5Kbg5%rr+`eX*Wotv z&M~=fk`gQF zv^z~s_8OV~Si*dee&O~S4z2)2P?uzf_^{_!#I1e+87~t;Xl(|riv&)d32#L# z+t1onH@~-(Lb`|kVNUx+&|{YZx9vp-*9h0YD-)iA?2CG02sB|;w|I{4GNG9o+E!#F zyAi<|fWYhevow+H3PMy~4{Hpsw;S*s|1;!Q?YOGi0&*pBcx?-)k$=2JVWXEN)_G`b}s935LcB) z7h!l^E60u4wg@|MXe&;`eGXy&YbNmkPJL3@p18k{JJev8nizW0;J+X6T@HVt;jz zCGOyQWcH2D^>Rz~K+fqck78w&qessIltJfnQ$!!G&)fgCQQKtZm%LL@hGlY7pkhCH zVT;;LFUV~J&GL0jU&csN85|F!avg{T->^2lfvZNXJiGSy7-xW;ZP^`rkcaH!3nhD&4&PNoKVTMx%{yMThUSt}Q|GN=jmrF$`~hq~>-K zk2Y#$PnNPn5S!!b3E?s-I=VL??Ak`qFz?KT?u8kL7U8!+dSfk^?Fuf85@>zS!dFQByogC9Z2!I>tC3wsm8onaG`7h2qcd2vVn z9xqTU*d`~Ee9H|a$VCcmMOx_oNBO1-XAeF(3RrM@M5sqtX>%RIWoZ5)u=DHq{fyu< ztdgGm#`JaRr`iVQ0f(aEs1DjAR zCmdPapavl1HXiOfkw+^pMDk=0==0`j%Df^wY}N+%Rj_(e#AwL-a7*ny2yMw~7tXbJ zeG*DhhQ;fP(&b`!uHJvIhZftM>jj_o9mHABVYr)D&zBbantu1-geKSb%=f9V@t@-Y zTgDw~&9^yw4&xpUz#_+ zIxbA_{PkqU!vlEdurjp8^&Xz}JszW5N8W|cq8}8p=hG{`(FNTs%b`bEtOIu2Z7>8z zjkMEVabn!m*YupsAA0VVk~+yNiodN^XmHb7tm2iPZ+vnDgpG*gOrSBsA8BNMceDv9 zlpWcy(GQI82`e5wZMylf8%gqzSQ7p_dsljErhB}o<>L>8EF*HkO2I^s{Sd{Pl{v4O zm=<(&?dKQfyn^c&nM7ArZ-$?My!uMst|@BAR5reMOcuEe*# zTl>77>Kzsr2%U2k!KIw3o9AADxO|nF@0s#cDm$c z=UFoyUfF+`&5O5?9#z7W$txFcJ5zne60+%_J$qE5ppJ>(w8F$Idy#&~||(&{d2l_Z$6OktIj z%h(AvO>(i1J)3-VG<-9sr-l&Z`{$WamHMSa(y~f-JxfyufyVJ#eWYFla)BNXBz|D5 zTOQmW;fJy!1xuehQW<`A0Z~G`FwbnF&n;b)jfzDfsZq{^W*b8%IKnwNtl;2}V$r)2 zml<0@)Jdg6OQ0U|S7 z@>msy7J^E(oc%pRPy6zn>17bqZ5}yto#9S?+L{x#x>=Iu@UR%ljf@pnW8W)HUd7%k z6l4aYDGiaK#wB|A&r}OM9QV;rnExX%)z5KwqzXfY88MP>U@ z!mLHMX@y>X=&CPFe^T-XG#0cX2uTGsAg!0<8BmY{bv5*4X;qPYOYH)V4W;hOm7Lvg zfk2yn#u;u)MQ*XPuI+SL*o2_|H0hhnJYZK74abDy4Ll8OMU3=7I!LEZ(H{l zRpZGLO}C&u4AfPl0J!J!yyuJ|E*FxSUH}y$G2Jrch2IHJG(&(gy{lrALC`<*(Cm0#V*}oOwBthy-gWF^azn{X|q8D-p*+vb+ z91JlcxkfjOtk|;?Jub|@a!Xbocv`k|lRA@TM=4i7*>_@ewc=;-__^vYxIhKQk)aK} zRepgxq)!+~iaCLh7FC~E{-d|6)#Uj1E(Vdlkw84-q=4yIQI3QFyoL+rF2zv zll;IBbK0fE-nq2iUk)-X*VPW2)q3;PO5iNA`}uO92BMd@LRZZp{H92uN%F%{1B&nv z;&Co)l={~moB}VndH}IiZ^?UBHv3nVgIg%fTgmD45Zoy3{4leSp*VpX5a!+K-TZ8m zm43d)wG3pQuzGJsnst7DS>ATLbvLW~`4UAqPzSJ8GA+qUn||-z`>)8EjH5Ab!=3@T zZ=DQCDjxm1?qi=ZPXtRvIAJo>XF|cH5avxuIdL?uWh4as#gN|(M%-cy&D0fbI3iv5 zWJKNq>W{COHNvMe(ky=|6}wpOsaK6K=69Iesh0`_W+MxDqJJH_==gVT@F~u*{X=g z_uTpapKz^yHZFJlqOcT4M{YcC8RjF@=4!Od>GqJ5CB`UT%u#V)(EM|@9;tx4!HlWB zWBfkmRZdd&uF0H2)_Try^sQBA0@vieq`n?apa*#lDW8o1 z?;N9>(7VCg8)XHH7nXO@z9_xq5hWFfQdfClCr4=+Y#!dYAN1^Ra|QXUC7(A}$G@@L g_>s||2L?CvSOJtU<6ovW!o(J!siv!1rEDGcf4}>;%m4rY From f9b87248c1fbc92c03e1b6a69cbd4b7c08fe5adc Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 18:21:29 +0900 Subject: [PATCH 02/36] chore: remove unused file --- .../app/src/shared/api/fetchClientFactory.ts | 315 ------------------ 1 file changed, 315 deletions(-) delete mode 100644 service/app/src/shared/api/fetchClientFactory.ts diff --git a/service/app/src/shared/api/fetchClientFactory.ts b/service/app/src/shared/api/fetchClientFactory.ts deleted file mode 100644 index 8ac81339..00000000 --- a/service/app/src/shared/api/fetchClientFactory.ts +++ /dev/null @@ -1,315 +0,0 @@ -// import ky, { type OptionsInit } from "ky" - -// import type { ResponsePromise } from "@/shared/api/type" - -// export type RequestInterceptor = { -// onFulfilled: ( -// url: string, -// options: RequestInit, -// ) => Promise<{ url: string; options: RequestInit }> -// onRejected?: (error: unknown) => Promise -// } - -// export type ResponseInterceptor = { -// // eslint-disable-next-line @typescript-eslint/no-explicit-any -// onFulfilled: (response: any) => any -// onRejected?: (error: unknown) => Promise -// } - -// type RequestExecutor = ( -// path: string, -// options?: RequestInit, -// ) => ResponsePromise - -// type GetOptions = Omit -// type MutateOptions = Omit - -// const toHeaderObject = (headers?: HeadersInit): Record => { -// if (!headers) { -// return {} -// } - -// if (headers instanceof Headers) { -// const result: Record = {} -// headers.forEach((value, key) => { -// result[key] = value -// }) -// return result -// } - -// if (Array.isArray(headers)) { -// return headers.reduce>((acc, [key, value]) => { -// acc[key] = value -// return acc -// }, {}) -// } - -// return headers -// } - -// const normalizeBody = (value: unknown): BodyInit | undefined => { -// if (value === undefined) { -// return undefined -// } - -// if (value === null) { -// return "null" -// } - -// if (typeof value === "string") { -// return value -// } - -// if (typeof FormData !== "undefined" && value instanceof FormData) { -// return value -// } - -// if ( -// typeof URLSearchParams !== "undefined" && -// value instanceof URLSearchParams -// ) { -// return value -// } - -// if (typeof Blob !== "undefined" && value instanceof Blob) { -// return value -// } - -// return JSON.stringify(value) -// } - -// const createMutateInit = ( -// method: string, -// body: B | undefined, -// options?: MutateOptions, -// ): RequestInit => { -// const { body: optionsBody, ...rest } = options || {} -// const payload = body === undefined ? optionsBody : normalizeBody(body) - -// const init: RequestInit = { -// ...rest, -// method, -// } - -// if (payload !== undefined) { -// init.body = payload -// } - -// return init -// } - -// const createResponsePromise = ( -// executor: () => Promise, -// ): ResponsePromise => { -// const rawPromise = executor().then((response) => { -// const augmented = Object.assign(response, { -// json: () => response.clone().json() as Promise, -// }) as Response & { json: () => Promise } - -// return augmented -// }) - -// const responsePromise = rawPromise as ResponsePromise - -// responsePromise.arrayBuffer = () => -// rawPromise.then((response) => response.clone().arrayBuffer()) - -// responsePromise.blob = () => -// rawPromise.then((response) => response.clone().blob()) - -// responsePromise.formData = () => -// rawPromise.then((response) => response.clone().formData()) - -// responsePromise.bytes = () => -// rawPromise.then(async (response) => { -// const buffer = await response.clone().arrayBuffer() -// return new Uint8Array(buffer) -// }) - -// responsePromise.json = () => rawPromise.then((response) => response.json()) - -// responsePromise.text = () => -// rawPromise.then((response) => response.clone().text()) - -// return responsePromise -// } - -// export interface FetchClientOptions { -// baseUrl: string -// defaultHeaders?: Record -// timeout?: number -// credentials?: RequestCredentials -// } - -// export interface FetchClientInstance { -// request: RequestExecutor -// fetch: RequestExecutor -// get: (path: string, options?: GetOptions) => ResponsePromise -// head: (path: string, options?: GetOptions) => ResponsePromise -// delete: ( -// path: string, -// options?: MutateOptions, -// ) => ResponsePromise -// post: ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => ResponsePromise -// put: ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => ResponsePromise -// patch: ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => ResponsePromise -// addRequestInterceptor: ( -// onFulfilled: RequestInterceptor["onFulfilled"], -// onRejected?: RequestInterceptor["onRejected"], -// ) => void -// addResponseInterceptor: ( -// onFulfilled: ResponseInterceptor["onFulfilled"], -// onRejected?: ResponseInterceptor["onRejected"], -// ) => void -// removeRequestInterceptor: (index: number) => void -// removeResponseInterceptor: (index: number) => void -// } - -// export const createFetchClient = ({ -// baseUrl, -// defaultHeaders = {}, -// timeout, -// credentials, -// }: FetchClientOptions): FetchClientInstance => { -// const requestInterceptors: RequestInterceptor[] = [] -// const responseInterceptors: ResponseInterceptor[] = [] - -// const baseKyOptions: OptionsInit = { -// throwHttpErrors: false, -// retry: { -// limit: 0, -// }, -// ...(timeout !== undefined ? { timeout } : {}), -// ...(credentials ? { credentials } : {}), -// } - -// const addRequestInterceptor = ( -// onFulfilled: RequestInterceptor["onFulfilled"], -// onRejected?: RequestInterceptor["onRejected"], -// ) => { -// requestInterceptors.push({ onFulfilled, onRejected }) -// } - -// const addResponseInterceptor = ( -// onFulfilled: ResponseInterceptor["onFulfilled"], -// onRejected?: ResponseInterceptor["onRejected"], -// ) => { -// responseInterceptors.push({ onFulfilled, onRejected }) -// } - -// const removeRequestInterceptor = (index: number) => { -// if (index >= 0 && index < requestInterceptors.length) -// requestInterceptors.splice(index, 1) -// } - -// const removeResponseInterceptor = (index: number) => { -// if (index >= 0 && index < responseInterceptors.length) -// responseInterceptors.splice(index, 1) -// } - -// const executeRequest = ( -// path: string, -// options: RequestInit = {}, -// ): ResponsePromise => -// createResponsePromise(async () => { -// let url = new URL(path, baseUrl).toString() -// const { headers: optionHeaders, ...restOptions } = options -// let fetchOptions: RequestInit = { -// ...restOptions, -// headers: { ...defaultHeaders, ...toHeaderObject(optionHeaders) }, -// ...(credentials && { credentials }), -// } - -// for (const { onFulfilled, onRejected } of requestInterceptors) { -// try { -// const result = await onFulfilled(url, fetchOptions) -// url = result.url -// fetchOptions = result.options -// } catch (error) { -// if (onRejected) await onRejected(error) -// else throw error -// } -// } - -// let response: Response -// try { -// response = await ky(url, { -// ...baseKyOptions, -// ...fetchOptions, -// headers: fetchOptions.headers, -// }) -// } catch (error) { -// for (const { onRejected } of responseInterceptors) { -// if (onRejected) await onRejected(error) -// } -// throw error -// } - -// let processed: Response = response - -// for (const { onFulfilled, onRejected } of responseInterceptors) { -// try { -// processed = await onFulfilled(processed) -// } catch (error) { -// if (onRejected) await onRejected(error) -// else throw error -// } -// } - -// return processed -// }) - -// const get = (path: string, options?: GetOptions) => -// executeRequest(path, { ...(options || {}), method: "GET" }) - -// const head = (path: string, options?: GetOptions) => -// executeRequest(path, { ...(options || {}), method: "HEAD" }) - -// const del = (path: string, options?: MutateOptions) => -// executeRequest(path, createMutateInit("DELETE", undefined, options)) - -// const post = ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => executeRequest(path, createMutateInit("POST", body, options)) - -// const put = ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => executeRequest(path, createMutateInit("PUT", body, options)) - -// const patchRequest = ( -// path: string, -// body?: B, -// options?: MutateOptions, -// ) => executeRequest(path, createMutateInit("PATCH", body, options)) - -// return { -// request: executeRequest, -// fetch: executeRequest, -// get, -// head, -// delete: del, -// post, -// put, -// patch: patchRequest, -// addRequestInterceptor, -// addResponseInterceptor, -// removeRequestInterceptor, -// removeResponseInterceptor, -// } -// } From 38fc9a3b6d769f5e6163df728bb9876b7ea25031 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 18:22:27 +0900 Subject: [PATCH 03/36] feat(admin): add navigation ui --- .../app/admin/_components/adminNavigation.tsx | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 service/app/src/app/admin/_components/adminNavigation.tsx diff --git a/service/app/src/app/admin/_components/adminNavigation.tsx b/service/app/src/app/admin/_components/adminNavigation.tsx new file mode 100644 index 00000000..3dbcc65f --- /dev/null +++ b/service/app/src/app/admin/_components/adminNavigation.tsx @@ -0,0 +1,31 @@ +import { Navigation } from "@ject-5-fe/design/components/navigation" + +import { HomeButton } from "@/widgets/components/homeButton" + +export const AdminNavigation = () => { + return ( + + + + 관리자페이지 + + + } + rightContent={ +
+ + 신고접수 + + + 게임관리 + + + 회원관리 + +
+ } + /> + ) +} From eff0943da1cc9d7006b088e8c7ea47aaffbee6db Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 18:26:05 +0900 Subject: [PATCH 04/36] feat(admin): add auth guard --- service/app/src/app/admin/layout.tsx | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 service/app/src/app/admin/layout.tsx diff --git a/service/app/src/app/admin/layout.tsx b/service/app/src/app/admin/layout.tsx new file mode 100644 index 00000000..31e4e62f --- /dev/null +++ b/service/app/src/app/admin/layout.tsx @@ -0,0 +1,42 @@ +"use client" + +import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" + +import { validateSession } from "@/entities/auth/api/validateSession" + +import { AdminNavigation } from "./_components/adminNavigation" + +export default function AdminLayout({ + children, +}: { + children: React.ReactNode +}) { + const router = useRouter() + const [checked, setChecked] = useState(false) + + useEffect(() => { + validateSession() + .then((user) => { + if (!user || user.data?.role !== "ADMIN") { + router.replace("/") + return + } + setChecked(true) + }) + .catch(() => { + router.replace("/") + }) + }, [router]) + + if (!checked) { + return null + } + + return ( +
+ + {children} +
+ ) +} From 2ef3e4c9b70b12ad95bd5c4f26b53b547b289375 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 18:38:12 +0900 Subject: [PATCH 05/36] feat(admin): add link / separate className --- .../app/admin/_components/adminNavigation.tsx | 43 +++++++++++++++---- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/service/app/src/app/admin/_components/adminNavigation.tsx b/service/app/src/app/admin/_components/adminNavigation.tsx index 3dbcc65f..acdc8db2 100644 --- a/service/app/src/app/admin/_components/adminNavigation.tsx +++ b/service/app/src/app/admin/_components/adminNavigation.tsx @@ -1,8 +1,24 @@ +import { SecondaryPlainBoxButton } from "@ject-5-fe/design/components/button" import { Navigation } from "@ject-5-fe/design/components/navigation" +import { cn } from "@ject-5-fe/design/utils/cn" +import Link from "next/link" +import { usePathname } from "next/navigation" import { HomeButton } from "@/widgets/components/homeButton" export const AdminNavigation = () => { + const pathname = usePathname() + + const isActive = (href: string) => + pathname === href || pathname.startsWith(`${href}/`) + + const navItemClass = (active: boolean) => + cn( + "typography-heading-md-semibold", + active + ? "text-text-interactive-secondary" + : "text-text-interactive-tertiary", + ) return ( { } rightContent={
- - 신고접수 - - - 게임관리 - - - 회원관리 - + + 신고접수 + + + 게임관리 + + + 회원관리 +
} /> From f8ad64f13708e61eb51d16ad94fc793e4be65b64 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Thu, 15 Jan 2026 18:42:18 +0900 Subject: [PATCH 06/36] feat(admin): use /admin as alias --- service/app/src/app/admin/page.tsx | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 service/app/src/app/admin/page.tsx diff --git a/service/app/src/app/admin/page.tsx b/service/app/src/app/admin/page.tsx new file mode 100644 index 00000000..f02040c3 --- /dev/null +++ b/service/app/src/app/admin/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation" + +export default function AdminPage() { + redirect("/admin/reports") +} From 72ffc7f0a7b3a7f2c85d619bde4818ff5cdd7992 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Sat, 17 Jan 2026 15:59:59 +0900 Subject: [PATCH 07/36] feat(admin/users): add block, unblock confirm dialog --- .../users/_components/blockConfirmModal.tsx | 56 +++++++++++++++++++ .../users/_components/unblockConfirmModal.tsx | 55 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 service/app/src/app/admin/users/_components/blockConfirmModal.tsx create mode 100644 service/app/src/app/admin/users/_components/unblockConfirmModal.tsx diff --git a/service/app/src/app/admin/users/_components/blockConfirmModal.tsx b/service/app/src/app/admin/users/_components/blockConfirmModal.tsx new file mode 100644 index 00000000..eab92c33 --- /dev/null +++ b/service/app/src/app/admin/users/_components/blockConfirmModal.tsx @@ -0,0 +1,56 @@ +import { + Dialog, + DialogBody, + DialogButton, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, +} from "@ject-5-fe/design/components/dialog" +import { overlay } from "overlay-kit" + +interface BlockConfirmModalOptions { + onConfirm?: () => void + onCancel?: () => void + confirmText?: string + cancelText?: string +} + +export function openBlockConfirmModal({ + onConfirm, + onCancel, + confirmText = "예", + cancelText = "아니요", +}: BlockConfirmModalOptions = {}) { + overlay.open(({ isOpen, close }) => ( + + + 선택한 사용자를{"\n"}차단하시겠습니까? + + 차단 처리 시 해당 계정은 게임 생성 및 공유, 신고 기능에 대한 접근 + 권한을 즉시 상실합니다. + + + + { + onCancel?.() + close() + }} + > + {cancelText} + + + { + onConfirm?.() + close() + }} + > + {confirmText} + + + + + )) +} diff --git a/service/app/src/app/admin/users/_components/unblockConfirmModal.tsx b/service/app/src/app/admin/users/_components/unblockConfirmModal.tsx new file mode 100644 index 00000000..a7d7c0a8 --- /dev/null +++ b/service/app/src/app/admin/users/_components/unblockConfirmModal.tsx @@ -0,0 +1,55 @@ +import { + Dialog, + DialogBody, + DialogButton, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, +} from "@ject-5-fe/design/components/dialog" +import { overlay } from "overlay-kit" + +interface UnblockConfirmModalOptions { + onConfirm?: () => void + onCancel?: () => void + confirmText?: string + cancelText?: string +} + +export function openUnblockConfirmModal({ + onConfirm, + onCancel, + confirmText = "예", + cancelText = "아니요", +}: UnblockConfirmModalOptions = {}) { + overlay.open(({ isOpen, close }) => ( + + + 선택한 사용자를{"\n"}차단 해제하시겠습니까? + + 차단 해제 즉시 사용자의 모든 이용 제한이 해제됩니다. + + + + { + onCancel?.() + close() + }} + > + {cancelText} + + + { + onConfirm?.() + close() + }} + > + {confirmText} + + + + + )) +} From ce65ea9a77d69eda7734b31f13e8a6fdc5f984ff Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Sat, 17 Jan 2026 19:09:18 +0900 Subject: [PATCH 08/36] feat(msw): add auth handler --- service/app/src/mocks/handlers/auth.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/service/app/src/mocks/handlers/auth.ts b/service/app/src/mocks/handlers/auth.ts index bce40d41..89f5de20 100644 --- a/service/app/src/mocks/handlers/auth.ts +++ b/service/app/src/mocks/handlers/auth.ts @@ -2,7 +2,9 @@ import { http, HttpResponse } from "msw" import { KakaoLoginRequest } from "@/entities/auth/model/authRequest" -import { kakaoLoginSuccess } from "../data/auth" +import { kakaoLoginSuccess, userInfoSuccess } from "../data/auth" +import { loginRequiredError } from "../data/common" +import { validateSessionCookie } from "../utils/gameHandlers" const MSW_BASE_URL = process.env.MSW_BASE_URL || "http://localhost:3000" export const MSW_MOCK_CODE = "someValidCode" @@ -36,4 +38,16 @@ export const authHandlers = [ }, ) }), + http.get(`${MSW_BASE_URL}/auth/me`, async ({ request }) => { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + return new HttpResponse(JSON.stringify(userInfoSuccess), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }) + }), ] From a7f87e5ab81d82b6178498b6c429961b4cbd335c Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Sat, 17 Jan 2026 19:09:52 +0900 Subject: [PATCH 09/36] fix(figma-plugin): apply sanitation for figma.mixed --- shared/figma-plugin/src/main.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shared/figma-plugin/src/main.ts b/shared/figma-plugin/src/main.ts index cef9de5d..7c81058c 100644 --- a/shared/figma-plugin/src/main.ts +++ b/shared/figma-plugin/src/main.ts @@ -6,6 +6,7 @@ import { rectangleNodeToReactNode, textNodeToReactNode, } from "./utils/node" +import { sanitizeForPostMessage } from "./utils/sanitize" // WebSocket 상태 관리 const state = { @@ -35,7 +36,7 @@ export default function () { figma.ui.postMessage({ type: "command-result", id: msg.id, - result, + result: sanitizeForPostMessage(result), }) } catch (error: unknown) { const errorMessage = @@ -116,11 +117,11 @@ export default function () { const xmlData = reactNodes.map((node) => reactNodeToXML(node)) figma.ui.postMessage({ type: "SELECTION_DATA", - data: { + data: sanitizeForPostMessage({ reactNodes, variables: variablesData, xml: xmlData, - }, + }), }) } catch (error: unknown) { const errorMessage = From b773d404aadd47f00efd36f3f080ec33d96d9393 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Sat, 17 Jan 2026 19:11:23 +0900 Subject: [PATCH 10/36] feat(admin): add admin report handling --- .../src/entities/auth/api/validateSession.ts | 11 +- service/app/src/entities/auth/model/auth.ts | 11 ++ service/app/src/mocks/data/admin.ts | 105 ++++++++++++++++++ service/app/src/mocks/data/auth.ts | 7 ++ service/app/src/mocks/handlers/admin.ts | 60 ++++++++++ service/app/src/mocks/handlers/index.ts | 3 +- 6 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 service/app/src/mocks/data/admin.ts create mode 100644 service/app/src/mocks/handlers/admin.ts diff --git a/service/app/src/entities/auth/api/validateSession.ts b/service/app/src/entities/auth/api/validateSession.ts index 58f37314..0a4d918d 100644 --- a/service/app/src/entities/auth/api/validateSession.ts +++ b/service/app/src/entities/auth/api/validateSession.ts @@ -1,6 +1,9 @@ import { fetchClient } from "@/shared/api/fetchClient" -export const validateSession = async () => { - const response = await fetchClient.get("users/me/games?limit=1") // TODO: 엔드포인트 생성 후 변경 - return response.json() -} +import { UserSessionResponse } from "../model/auth" + +export const validateSession = + async (): Promise => { + const response = await fetchClient.get("auth/me") + return response.json() + } diff --git a/service/app/src/entities/auth/model/auth.ts b/service/app/src/entities/auth/model/auth.ts index 9af71397..e6efd514 100644 --- a/service/app/src/entities/auth/model/auth.ts +++ b/service/app/src/entities/auth/model/auth.ts @@ -1,9 +1,20 @@ import type { ApiResponse } from "@/shared/api/types/response" +export type UserRole = "ADMIN" | "USER" | "BLOCKED" export interface KakaoLoginData { profileImageUrl: string nickname: string email: string + role: UserRole } export type KakaoLoginResponse = ApiResponse + +export interface UserSession { + profileImageUrl: string + nickname: string + email: string + role: UserRole +} + +export type UserSessionResponse = ApiResponse diff --git a/service/app/src/mocks/data/admin.ts b/service/app/src/mocks/data/admin.ts new file mode 100644 index 00000000..1314978c --- /dev/null +++ b/service/app/src/mocks/data/admin.ts @@ -0,0 +1,105 @@ +export type ReportStatus = "PENDING" | "RESOLVED" | "REJECTED" + +export interface AdminReport { + reportId: number + gameName: string + creatorName: string + reporterName: string + reportedAt: string + status: ReportStatus +} + +export interface AdminReportsResponse { + report: AdminReport[] + page: number + size: number + totalElements: number + totalPages: number + hasNext: boolean +} + +const gameNames = [ + "퀴즈 마스터", + "두뇌 게임", + "상식 퀴즈", + "역사 퀴즈", + "과학 퀴즈", + "스포츠 퀴즈", + "음악 퀴즈", + "영화 퀴즈", + "문학 퀴즈", + "지리 퀴즈", +] + +const creatorNames = [ + "홍길동", + "김철수", + "이영희", + "박민수", + "정수진", + "최동현", + "강미영", + "윤태준", + "임지은", + "한소연", +] + +const reporterNames = [ + "신고자1", + "신고자2", + "신고자3", + "신고자4", + "신고자5", + "신고자6", + "신고자7", + "신고자8", + "신고자9", + "신고자10", +] + +const statuses: ReportStatus[] = ["PENDING", "RESOLVED", "REJECTED"] + +export const generateMockAdminReports = ( + count: number, + seed?: number, +): AdminReport[] => { + let originalRandom: (() => number) | undefined + + if (seed !== undefined) { + originalRandom = Math.random + let currentSeed = seed + Math.random = () => { + const x = Math.sin(currentSeed++) * 10000 + return x - Math.floor(x) + } + } + + const result: AdminReport[] = [] + for (let i = 0; i < count; i++) { + const randomIndex = Math.floor(Math.random() * gameNames.length) + const creatorIndex = Math.floor(Math.random() * creatorNames.length) + const reporterIndex = Math.floor(Math.random() * reporterNames.length) + const statusIndex = Math.floor(Math.random() * statuses.length) + const daysAgo = Math.floor(Math.random() * 30) + + const date = new Date() + date.setDate(date.getDate() - daysAgo) + + result.push({ + reportId: i + 1, + gameName: gameNames[randomIndex], + creatorName: creatorNames[creatorIndex], + reporterName: reporterNames[reporterIndex], + reportedAt: date.toISOString(), + status: statuses[statusIndex], + }) + } + + if (originalRandom) { + Math.random = originalRandom + } + + return result +} + +export const mockAdminReports: AdminReport[] = generateMockAdminReports(21, 456) diff --git a/service/app/src/mocks/data/auth.ts b/service/app/src/mocks/data/auth.ts index f5efe106..210d9677 100644 --- a/service/app/src/mocks/data/auth.ts +++ b/service/app/src/mocks/data/auth.ts @@ -4,6 +4,7 @@ const mockKakaoLoginData: KakaoLoginData = { profileImageUrl: "/avatar.svg", nickname: "testUser", email: "test@example.com", + role: "ADMIN", } export const kakaoLoginSuccess: KakaoLoginResponse = { @@ -11,3 +12,9 @@ export const kakaoLoginSuccess: KakaoLoginResponse = { data: mockKakaoLoginData, error: null, } + +export const userInfoSuccess: KakaoLoginResponse = { + result: "SUCCESS", + data: mockKakaoLoginData, + error: null, +} diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts new file mode 100644 index 00000000..42003083 --- /dev/null +++ b/service/app/src/mocks/handlers/admin.ts @@ -0,0 +1,60 @@ +import { http, HttpResponse } from "msw" + +import { type AdminReportsResponse, mockAdminReports } from "../data/admin" +import { internalServerError, loginRequiredError } from "../data/common" +import { validateSessionCookie } from "../utils/gameHandlers" +import { generateSuccessResponse } from "../utils/responseHelpers" + +const MSW_BASE_URL = process.env.MSW_BASE_URL || "http://localhost:3000" + +const PAGE_SIZE = 7 +const TOTAL_ELEMENTS = 21 + +export const adminHandlers = [ + http.get(`${MSW_BASE_URL}/admin/games`, ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const url = new URL(request.url) + const pageParam = url.searchParams.get("page") + const page = pageParam ? Math.max(0, parseInt(pageParam, 10)) : 0 + + if (isNaN(page) || page < 0) { + return HttpResponse.json( + { + result: "ERROR", + data: null, + error: { + code: "E400", + message: "Invalid page parameter", + }, + }, + { status: 400 }, + ) + } + + const totalPages = Math.ceil(TOTAL_ELEMENTS / PAGE_SIZE) + const startIndex = page * PAGE_SIZE + const endIndex = startIndex + PAGE_SIZE + + const paginatedReports = mockAdminReports.slice(startIndex, endIndex) + const hasNext = page < totalPages - 1 + + const response: AdminReportsResponse = { + report: paginatedReports, + page, + size: PAGE_SIZE, + totalElements: TOTAL_ELEMENTS, + totalPages, + hasNext, + } + + return HttpResponse.json(generateSuccessResponse(response)) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) + } + }), +] diff --git a/service/app/src/mocks/handlers/index.ts b/service/app/src/mocks/handlers/index.ts index b1bae729..5b0488a6 100644 --- a/service/app/src/mocks/handlers/index.ts +++ b/service/app/src/mocks/handlers/index.ts @@ -1,4 +1,5 @@ +import { adminHandlers } from "./admin" import { authHandlers } from "./auth" import { gameHandlers } from "./game" -export const handlers = [...authHandlers, ...gameHandlers] +export const handlers = [...authHandlers, ...gameHandlers, ...adminHandlers] From 0cfea3ff61b8759d50f20eab1ed3cb94cfd4deae Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Sat, 17 Jan 2026 19:13:07 +0900 Subject: [PATCH 11/36] feat(admin): add basic routes --- service/app/src/app/admin/games/page.tsx | 3 +++ service/app/src/app/admin/reports/page.tsx | 11 +++++++++++ service/app/src/app/admin/users/page.tsx | 3 +++ 3 files changed, 17 insertions(+) create mode 100644 service/app/src/app/admin/games/page.tsx create mode 100644 service/app/src/app/admin/reports/page.tsx create mode 100644 service/app/src/app/admin/users/page.tsx diff --git a/service/app/src/app/admin/games/page.tsx b/service/app/src/app/admin/games/page.tsx new file mode 100644 index 00000000..82c7acb3 --- /dev/null +++ b/service/app/src/app/admin/games/page.tsx @@ -0,0 +1,3 @@ +export default function AdminGamesPage() { + return
AdminPage-Games
+} diff --git a/service/app/src/app/admin/reports/page.tsx b/service/app/src/app/admin/reports/page.tsx new file mode 100644 index 00000000..c5520024 --- /dev/null +++ b/service/app/src/app/admin/reports/page.tsx @@ -0,0 +1,11 @@ +"use client" + +import { Pagination } from "@ject-5-fe/design/components/pagination" + +export default function AdminReportsPage() { + return ( +
+ {}} /> +
+ ) +} diff --git a/service/app/src/app/admin/users/page.tsx b/service/app/src/app/admin/users/page.tsx new file mode 100644 index 00000000..e892220f --- /dev/null +++ b/service/app/src/app/admin/users/page.tsx @@ -0,0 +1,3 @@ +export default function AdminUsersPage() { + return
AdminPage-Users
+} From ec9b7f83334eae6fcbc26ab4d9e2ef1ae606feb7 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Mon, 19 Jan 2026 17:13:25 +0900 Subject: [PATCH 12/36] Revert "fix(figma-plugin): apply sanitation for figma.mixed" This reverts commit a7f87e5ab81d82b6178498b6c429961b4cbd335c. --- shared/figma-plugin/src/main.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/shared/figma-plugin/src/main.ts b/shared/figma-plugin/src/main.ts index 7c81058c..cef9de5d 100644 --- a/shared/figma-plugin/src/main.ts +++ b/shared/figma-plugin/src/main.ts @@ -6,7 +6,6 @@ import { rectangleNodeToReactNode, textNodeToReactNode, } from "./utils/node" -import { sanitizeForPostMessage } from "./utils/sanitize" // WebSocket 상태 관리 const state = { @@ -36,7 +35,7 @@ export default function () { figma.ui.postMessage({ type: "command-result", id: msg.id, - result: sanitizeForPostMessage(result), + result, }) } catch (error: unknown) { const errorMessage = @@ -117,11 +116,11 @@ export default function () { const xmlData = reactNodes.map((node) => reactNodeToXML(node)) figma.ui.postMessage({ type: "SELECTION_DATA", - data: sanitizeForPostMessage({ + data: { reactNodes, variables: variablesData, xml: xmlData, - }), + }, }) } catch (error: unknown) { const errorMessage = From d6bfd7d0db43294971c7d3e15f61c9a716642eb0 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Mon, 19 Jan 2026 17:21:21 +0900 Subject: [PATCH 13/36] refactor(gamepreview): update gamepreview component using composition support admin page gamepreview --- .../entities/game/hooks/useGamePreview.tsx | 4 +- .../game/ui/GamePreview/gamePreviewAdmin.tsx | 94 +++++++++++++ .../GamePreview/gamePreviewQuestionList.tsx | 45 ++++++ .../ui/GamePreview/gamePreviewReportTable.tsx | 78 +++++++++++ .../game/ui/GamePreview/gamePreviewShell.tsx | 42 ++++++ .../game/ui/GamePreview/gamePreviewTitle.tsx | 36 +++++ .../game/ui/GamePreview/gamePreviewUser.tsx | 63 +++++++++ .../src/entities/game/ui/GamePreview/index.ts | 6 + .../app/src/entities/game/ui/gamePreview.tsx | 132 ------------------ 9 files changed, 366 insertions(+), 134 deletions(-) create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewShell.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewTitle.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx create mode 100644 service/app/src/entities/game/ui/GamePreview/index.ts delete mode 100644 service/app/src/entities/game/ui/gamePreview.tsx diff --git a/service/app/src/entities/game/hooks/useGamePreview.tsx b/service/app/src/entities/game/hooks/useGamePreview.tsx index 051df13e..6c4962aa 100644 --- a/service/app/src/entities/game/hooks/useGamePreview.tsx +++ b/service/app/src/entities/game/hooks/useGamePreview.tsx @@ -6,7 +6,7 @@ import { useCallback } from "react" import type { GameListItem } from "@/entities/game" import { getGameDetail } from "@/entities/game/api/getGameDetail" import { GameQuestion } from "@/entities/game/model/game" -import { GamePreview } from "@/entities/game/ui/gamePreview" +import { GamePreviewUser } from "@/entities/game/ui/GamePreview" import { useGameEntryNavigation } from "./useGameEntryNavigation" @@ -29,7 +29,7 @@ export const useGamePreview = ({ onError }: UseGamePreviewParams = {}) => { } return ( - + reportData?: { + creator: { + name: string + email: string + } + reporter: { + name: string + email: string + } + category: string + } + onClose?: () => void + onIgnoreReport?: () => void + onDeleteGame?: () => void + onBlockCreator?: () => void + onBlockReporter?: () => void + isOpen?: boolean +} + +export const GamePreviewAdmin = ({ + className = "", + gameTitle, + questionCount = 10, + questions = [], + reportData, + onClose, + onIgnoreReport, + onDeleteGame, + onBlockCreator, + onBlockReporter, + isOpen = true, +}: GamePreviewAdminProps) => { + return ( + +
+
+ + +
+ +
+ + 신고 무시 + + + 게임 삭제 + +
+
+ + {reportData && ( + + )} +
+
+
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx new file mode 100644 index 00000000..aad70945 --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx @@ -0,0 +1,45 @@ +"use client" + +import * as GameCard from "@/entities/game/ui/GameCard/gameCard" + +interface GamePreviewQuestionListProps { + questionCount: number + questions: Array<{ + id: string + title: string + imageUrl?: string + }> +} + +export const GamePreviewQuestionList = ({ + questionCount, + questions, +}: GamePreviewQuestionListProps) => { + return ( +
+ + 총 {questionCount} 문제 + +
+ {questions.map((question) => ( + + + {question.title} + + ))} +
+
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx new file mode 100644 index 00000000..b53182d6 --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx @@ -0,0 +1,78 @@ +"use client" + +import { PrimaryBoxButton } from "@ject-5-fe/design/components/button" + +interface GamePreviewReportTableProps { + creator: { name: string; email: string } + reporter: { name: string; email: string } + category: string + onBlockCreator?: () => void + onBlockReporter?: () => void +} + +export const GamePreviewReportTable = ({ + creator, + reporter, + category, + onBlockCreator, + onBlockReporter, +}: GamePreviewReportTableProps) => { + return ( +
+
+
+ + 제작자 + +
+
+ + {creator.name}({creator.email}) + + + 차단 + +
+
+ +
+
+ + 신고자 + +
+
+ + {reporter.name}({reporter.email}) + + + 차단 + +
+
+ +
+
+ + 항목 + +
+
+ + {category} + +
+
+
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewShell.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewShell.tsx new file mode 100644 index 00000000..0a2347af --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewShell.tsx @@ -0,0 +1,42 @@ +"use client" + +import { SecondaryPlainIconButton } from "@ject-5-fe/design/components/button" +import { Dialog, DialogContent } from "@ject-5-fe/design/components/dialog" +import { Cross } from "@ject-5-fe/design/icons" +import type { PropsWithChildren } from "react" + +interface GamePreviewShellProps extends PropsWithChildren { + className?: string + isOpen?: boolean + onClose?: () => void +} + +export const GamePreviewShell = ({ + children, + className = "", + isOpen = true, + onClose, +}: GamePreviewShellProps) => { + const handleClose = () => { + onClose?.() + } + + return ( + + +
+ + + +
+ {children} +
+
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewTitle.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewTitle.tsx new file mode 100644 index 00000000..df29a691 --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewTitle.tsx @@ -0,0 +1,36 @@ +"use client" + +import { DialogHeader } from "@ject-5-fe/design/components/dialog" + +interface GamePreviewTitleProps { + gameTitle?: string + creatorName?: string + showCreatorName?: boolean +} + +export const GamePreviewTitle = ({ + gameTitle, + creatorName, + showCreatorName = true, +}: GamePreviewTitleProps) => { + return ( +
+ +

+ {gameTitle} +

+
+ {showCreatorName && creatorName && ( + + {creatorName} + + )} +
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx new file mode 100644 index 00000000..9e865fe1 --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx @@ -0,0 +1,63 @@ +"use client" + +import { PrimaryBoxButton } from "@ject-5-fe/design/components/button" +import { Play } from "@ject-5-fe/design/icons" + +import { GamePreviewQuestionList } from "./gamePreviewQuestionList" +import { GamePreviewShell } from "./gamePreviewShell" +import { GamePreviewTitle } from "./gamePreviewTitle" + +interface GamePreviewUserProps { + className?: string + gameTitle?: string + creatorName?: string + questionCount?: number + questions?: Array<{ + id: string + title: string + imageUrl?: string + }> + onClose?: () => void + onStartGame?: () => void + isOpen?: boolean +} + +export const GamePreviewUser = ({ + className = "", + gameTitle, + creatorName, + questionCount = 10, + questions = [], + onClose, + onStartGame, + isOpen = true, +}: GamePreviewUserProps) => { + const handleStartGame = () => { + onStartGame?.() + } + + return ( + +
+
+
+ + + + 게임 시작 + +
+ + +
+
+
+ ) +} diff --git a/service/app/src/entities/game/ui/GamePreview/index.ts b/service/app/src/entities/game/ui/GamePreview/index.ts new file mode 100644 index 00000000..562fd37a --- /dev/null +++ b/service/app/src/entities/game/ui/GamePreview/index.ts @@ -0,0 +1,6 @@ +export { GamePreviewAdmin } from "./gamePreviewAdmin" +export { GamePreviewQuestionList } from "./gamePreviewQuestionList" +export { GamePreviewReportTable } from "./gamePreviewReportTable" +export { GamePreviewShell } from "./gamePreviewShell" +export { GamePreviewTitle } from "./gamePreviewTitle" +export { GamePreviewUser } from "./gamePreviewUser" diff --git a/service/app/src/entities/game/ui/gamePreview.tsx b/service/app/src/entities/game/ui/gamePreview.tsx deleted file mode 100644 index d04d111d..00000000 --- a/service/app/src/entities/game/ui/gamePreview.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client" - -import { - PrimaryBoxButton, - SecondaryPlainIconButton, -} from "@ject-5-fe/design/components/button" -import { - Dialog, - DialogBody, - DialogContent, - DialogHeader, -} from "@ject-5-fe/design/components/dialog" -import { Cross, Play } from "@ject-5-fe/design/icons" - -import * as GameCard from "@/entities/game/ui/GameCard/gameCard" - -interface GamePreviewProps { - className?: string - gameTitle?: string - creatorName?: string - questionCount?: number - questions?: Array<{ - id: string - title: string - imageUrl?: string - }> - onClose?: () => void - onStartGame?: () => void - isOpen?: boolean -} - -export const GamePreview = ({ - className = "", - gameTitle, - creatorName, - questionCount = 10, - questions = [], - onClose, - onStartGame, - isOpen = true, -}: GamePreviewProps) => { - const handleClose = () => { - onClose?.() - } - - const handleStartGame = () => { - onStartGame?.() - } - - return ( - - -
- - - -
- -
-
-
-
- -

- {gameTitle} -

-
- - {creatorName} - -
- - - - 게임 시작 - -
- - -
- - 총 {questionCount} 문제 - -
- {questions.map((question) => ( - - - - {question.title} - - - ))} -
-
-
-
-
-
-
- ) -} From 899ff756c8da0f653c9cd691a658236cd27ab0df Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Mon, 19 Jan 2026 17:37:30 +0900 Subject: [PATCH 14/36] feat(components): add pagination --- .../src/components/pagination/index.tsx | 2 + .../src/components/pagination/pagination.tsx | 130 ++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 shared/design/src/components/pagination/index.tsx create mode 100644 shared/design/src/components/pagination/pagination.tsx diff --git a/shared/design/src/components/pagination/index.tsx b/shared/design/src/components/pagination/index.tsx new file mode 100644 index 00000000..c9f046df --- /dev/null +++ b/shared/design/src/components/pagination/index.tsx @@ -0,0 +1,2 @@ +export type { PaginationProps } from "./pagination" +export { Pagination } from "./pagination" diff --git a/shared/design/src/components/pagination/pagination.tsx b/shared/design/src/components/pagination/pagination.tsx new file mode 100644 index 00000000..9c811f15 --- /dev/null +++ b/shared/design/src/components/pagination/pagination.tsx @@ -0,0 +1,130 @@ +import { type ComponentPropsWithoutRef } from "react" +import { forwardRef } from "react" + +import { Arrow } from "../../icons" +import { cn } from "../../utils/cn" +import { SecondaryPlainBoxButton, SecondaryPlainIconButton } from "../button" + +export interface PaginationProps extends ComponentPropsWithoutRef<"nav"> { + currentPage: number + totalPages: number + onPageChange: (page: number) => void + maxVisible?: number +} + +interface PaginationItemProps { + page: number + isActive: boolean + onClick: () => void +} + +const PaginationItem = ({ page, isActive, onClick }: PaginationItemProps) => { + return ( + + {page} + + ) +} + +export const Pagination = forwardRef( + ( + { + currentPage, + totalPages, + onPageChange, + maxVisible = 5, + className, + ...rest + }, + ref, + ) => { + const handlePrev = () => { + if (currentPage > 1) { + onPageChange(currentPage - 1) + } + } + + const handleNext = () => { + if (currentPage < totalPages) { + onPageChange(currentPage + 1) + } + } + + const handlePageClick = (page: number) => { + if (page !== currentPage) { + onPageChange(page) + } + } + + const getVisiblePages = () => { + const pages: number[] = [] + const half = Math.floor(maxVisible / 2) + + let start = Math.max(1, currentPage - half) + const end = Math.min(totalPages, start + maxVisible - 1) + + if (end - start < maxVisible - 1) { + start = Math.max(1, end - maxVisible + 1) + } + + for (let i = start; i <= end; i++) { + pages.push(i) + } + + return pages + } + + const visiblePages = getVisiblePages() + const isPrevDisabled = currentPage <= 1 + const isNextDisabled = currentPage >= totalPages + + return ( + + ) + }, +) + +Pagination.displayName = "Pagination" From 5dc73d2a6a205707042faec7f7834ca42b89ab77 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 16:55:00 +0900 Subject: [PATCH 15/36] style(pagination): update selected style --- shared/design/src/components/pagination/pagination.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared/design/src/components/pagination/pagination.tsx b/shared/design/src/components/pagination/pagination.tsx index 9c811f15..a811434c 100644 --- a/shared/design/src/components/pagination/pagination.tsx +++ b/shared/design/src/components/pagination/pagination.tsx @@ -23,8 +23,8 @@ const PaginationItem = ({ page, isActive, onClick }: PaginationItemProps) => { Date: Tue, 20 Jan 2026 16:57:01 +0900 Subject: [PATCH 16/36] feat(admin): add reported games table --- .../shared/ui/admin-table/AdminTableGroup.tsx | 16 +++++++ .../shared/ui/admin-table/AdminTableRoot.tsx | 20 +++++++++ .../shared/ui/admin-table/AdminTableRow.tsx | 25 +++++++++++ .../shared/ui/admin-table/AdminTableText.tsx | 32 ++++++++++++++ .../app/src/shared/ui/admin-table/index.ts | 5 +++ .../admin/reported-games-table/index.ts | 1 + .../ui/ReportedGameRow.tsx | 33 ++++++++++++++ .../ui/ReportedGamesTable.tsx | 44 +++++++++++++++++++ 8 files changed, 176 insertions(+) create mode 100644 service/app/src/shared/ui/admin-table/AdminTableGroup.tsx create mode 100644 service/app/src/shared/ui/admin-table/AdminTableRoot.tsx create mode 100644 service/app/src/shared/ui/admin-table/AdminTableRow.tsx create mode 100644 service/app/src/shared/ui/admin-table/AdminTableText.tsx create mode 100644 service/app/src/shared/ui/admin-table/index.ts create mode 100644 service/app/src/widgets/admin/reported-games-table/index.ts create mode 100644 service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx create mode 100644 service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx diff --git a/service/app/src/shared/ui/admin-table/AdminTableGroup.tsx b/service/app/src/shared/ui/admin-table/AdminTableGroup.tsx new file mode 100644 index 00000000..8bebd95b --- /dev/null +++ b/service/app/src/shared/ui/admin-table/AdminTableGroup.tsx @@ -0,0 +1,16 @@ +import { cn } from "@ject-5-fe/design/utils/cn" +import type { ReactNode } from "react" + +interface AdminTableGroupProps { + children: ReactNode + className?: string +} + +export const AdminTableGroup = ({ + children, + className, +}: AdminTableGroupProps) => { + return ( +
{children}
+ ) +} diff --git a/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx b/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx new file mode 100644 index 00000000..8389f581 --- /dev/null +++ b/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx @@ -0,0 +1,20 @@ +import { cn } from "@ject-5-fe/design/utils/cn" +import type { ReactNode } from "react" + +interface AdminTableRootProps { + children: ReactNode + className?: string +} + +export const AdminTableRoot = ({ + children, + className, +}: AdminTableRootProps) => { + return ( +
+
+
{children}
+
+
+ ) +} diff --git a/service/app/src/shared/ui/admin-table/AdminTableRow.tsx b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx new file mode 100644 index 00000000..ca684d5d --- /dev/null +++ b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx @@ -0,0 +1,25 @@ +import { cn } from "@ject-5-fe/design/utils/cn" +import type { ReactNode } from "react" + +type AdminTableRowVariant = "header" | "data" + +interface AdminTableRowProps { + children: ReactNode + className?: string + variant?: AdminTableRowVariant +} + +export const AdminTableRow = ({ + children, + className, + variant = "data", +}: AdminTableRowProps) => { + const base = "flex items-center py-20 min-w-0 px-16" + const border = "border border-border-interactive-tertiary" + const bg = + variant === "header" + ? "bg-background-interactive-secondary-hovered" + : "bg-transparent" + + return
{children}
+} diff --git a/service/app/src/shared/ui/admin-table/AdminTableText.tsx b/service/app/src/shared/ui/admin-table/AdminTableText.tsx new file mode 100644 index 00000000..5a778ac3 --- /dev/null +++ b/service/app/src/shared/ui/admin-table/AdminTableText.tsx @@ -0,0 +1,32 @@ +import { cn } from "@ject-5-fe/design/utils/cn" +import type { ReactNode } from "react" + +type AdminTableTextTone = "header" | "body" + +interface AdminTableTextProps { + children: ReactNode + className?: string + tone?: AdminTableTextTone +} + +export const AdminTableText = ({ + children, + className, + tone = "body", +}: AdminTableTextProps) => { + const header = + "typography-heading-md-medium text-text-interactive-secondary-hovered" + const body = "typography-heading-md-medium text-text-interactive-input-filled" + + return ( + + {children} + + ) +} diff --git a/service/app/src/shared/ui/admin-table/index.ts b/service/app/src/shared/ui/admin-table/index.ts new file mode 100644 index 00000000..eb50b8a0 --- /dev/null +++ b/service/app/src/shared/ui/admin-table/index.ts @@ -0,0 +1,5 @@ +export * from "./AdminTableGroup" +export * from "./AdminTableRoot" +export * from "./AdminTableRow" +export * from "./AdminTableText" +export * from "./useRowSelection" diff --git a/service/app/src/widgets/admin/reported-games-table/index.ts b/service/app/src/widgets/admin/reported-games-table/index.ts new file mode 100644 index 00000000..f615a157 --- /dev/null +++ b/service/app/src/widgets/admin/reported-games-table/index.ts @@ -0,0 +1 @@ +export * from "./ui/ReportedGamesTable" diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx new file mode 100644 index 00000000..a2e24aae --- /dev/null +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx @@ -0,0 +1,33 @@ +import type { ReportedGame } from "@/entities/report/model/types" +import { + AdminTableGroup, + AdminTableRow, + AdminTableText, +} from "@/shared/ui/admin-table" + +interface ReportedGameRowProps { + item: ReportedGame +} + +export const ReportedGameRow = ({ item }: ReportedGameRowProps) => { + return ( + + + {item.no} + {item.title} + + + + {item.creatorName} + {item.reporterName} + + + + + {item.reportedAt} + + {item.statusLabel} + + + ) +} diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx new file mode 100644 index 00000000..53d73334 --- /dev/null +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx @@ -0,0 +1,44 @@ +import type { ReportedGame } from "@/entities/report/model/types" +import { + AdminTableGroup, + AdminTableRoot, + AdminTableRow, + AdminTableText, +} from "@/shared/ui/admin-table" + +import { ReportedGameRow } from "./ReportedGameRow" + +interface ReportedGamesTableProps { + items: ReportedGame[] +} + +export const ReportedGamesTable = ({ items }: ReportedGamesTableProps) => { + return ( + + + + + 번호 + + 게임명 + + + + 제작자 + 신고자 + + + + + 신고일자 + + 처리 여부 + + + + {items.map((item) => ( + + ))} + + ) +} From 273521a2e9aaaf94d813d9ff965fc0fef2be68a8 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 16:57:55 +0900 Subject: [PATCH 17/36] feat(admin): add table ui to reports page --- service/app/src/app/admin/reports/page.tsx | 30 +++++++++++- .../entities/report/api/getAdminReports.ts | 18 +++++++ service/app/src/entities/report/api/index.ts | 1 + .../app/src/entities/report/hooks/index.ts | 1 + .../entities/report/hooks/useAdminReports.ts | 49 +++++++++++++++++++ .../app/src/entities/report/model/types.ts | 27 ++++++++++ .../entities/report/utils/formatReportDate.ts | 7 +++ .../report/utils/formatReportNumber.ts | 3 ++ .../utils/mapAdminReportToReportedGame.ts | 27 ++++++++++ 9 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 service/app/src/entities/report/api/getAdminReports.ts create mode 100644 service/app/src/entities/report/api/index.ts create mode 100644 service/app/src/entities/report/hooks/index.ts create mode 100644 service/app/src/entities/report/hooks/useAdminReports.ts create mode 100644 service/app/src/entities/report/model/types.ts create mode 100644 service/app/src/entities/report/utils/formatReportDate.ts create mode 100644 service/app/src/entities/report/utils/formatReportNumber.ts create mode 100644 service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts diff --git a/service/app/src/app/admin/reports/page.tsx b/service/app/src/app/admin/reports/page.tsx index c5520024..bb0debb5 100644 --- a/service/app/src/app/admin/reports/page.tsx +++ b/service/app/src/app/admin/reports/page.tsx @@ -1,11 +1,37 @@ "use client" import { Pagination } from "@ject-5-fe/design/components/pagination" +import { useState } from "react" + +import { useAdminReports } from "@/entities/report/hooks/useAdminReports" +import { ReportedGamesTable } from "@/widgets/admin/reported-games-table" export default function AdminReportsPage() { + const [currentPage, setCurrentPage] = useState(1) + const pageIndex = currentPage - 1 + + const { games, totalPages } = useAdminReports({ + page: pageIndex, + }) + + const handlePageChange = (page: number) => { + setCurrentPage(page) + } + return ( -
- {}} /> +
+
+

신고접수

+ +
+ + {totalPages > 0 && ( + + )}
) } diff --git a/service/app/src/entities/report/api/getAdminReports.ts b/service/app/src/entities/report/api/getAdminReports.ts new file mode 100644 index 00000000..a3883185 --- /dev/null +++ b/service/app/src/entities/report/api/getAdminReports.ts @@ -0,0 +1,18 @@ +import { fetchClient } from "@/shared/api/fetchClient" + +import type { AdminReportsResponse } from "../model/types" + +export interface GetAdminReportsParams { + page?: number +} + +export const getAdminReports = async ( + params: GetAdminReportsParams = {}, +): Promise => { + const { page = 0 } = params + const queryString = page > 0 ? `?page=${page}` : "" + const response = await fetchClient.get( + `admin/games${queryString}`, + ) + return response.json().then((res) => res.data) +} diff --git a/service/app/src/entities/report/api/index.ts b/service/app/src/entities/report/api/index.ts new file mode 100644 index 00000000..88bccb5f --- /dev/null +++ b/service/app/src/entities/report/api/index.ts @@ -0,0 +1 @@ +export * from "./getAdminReports" diff --git a/service/app/src/entities/report/hooks/index.ts b/service/app/src/entities/report/hooks/index.ts new file mode 100644 index 00000000..97e7dc3d --- /dev/null +++ b/service/app/src/entities/report/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useAdminReports" diff --git a/service/app/src/entities/report/hooks/useAdminReports.ts b/service/app/src/entities/report/hooks/useAdminReports.ts new file mode 100644 index 00000000..23647f3f --- /dev/null +++ b/service/app/src/entities/report/hooks/useAdminReports.ts @@ -0,0 +1,49 @@ +import { useQuery } from "@tanstack/react-query" + +import { getAdminReports } from "../api/getAdminReports" +import type { ReportedGame } from "../model/types" +import { mapAdminReportToReportedGame } from "../utils/mapAdminReportToReportedGame" + +interface UseAdminReportsParams { + page?: number + enabled?: boolean +} + +interface UseAdminReportsReturn { + games: ReportedGame[] + currentPage: number + totalPages: number + totalElements: number + isLoading: boolean + error: Error | null +} + +export const useAdminReports = ({ + page = 0, + enabled = true, +}: UseAdminReportsParams = {}): UseAdminReportsReturn => { + const { data, isLoading, error } = useQuery({ + queryKey: ["adminReports", { page }] as const, + queryFn: async () => { + const response = await getAdminReports({ page }) + return response + }, + enabled, + staleTime: 5 * 60 * 1000, + retry: 2, + }) + + const games: ReportedGame[] = + data?.report.map((report, index) => + mapAdminReportToReportedGame(report, index, page, data.size), + ) ?? [] + + return { + games, + currentPage: data?.page ?? page, + totalPages: data?.totalPages ?? 0, + totalElements: data?.totalElements ?? 0, + isLoading, + error: error as Error | null, + } +} diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts new file mode 100644 index 00000000..97270606 --- /dev/null +++ b/service/app/src/entities/report/model/types.ts @@ -0,0 +1,27 @@ +export interface ReportedGame { + id: string + no: number | string + title: string + creatorName: string + reporterName: string + reportedAt: string + statusLabel: string +} + +export interface AdminReportsResponse { + report: AdminReport[] + page: number + size: number + totalElements: number + totalPages: number + hasNext: boolean +} + +export interface AdminReport { + reportId: number + gameName: string + creatorName: string + reporterName: string + reportedAt: string + status: "PENDING" | "RESOLVED" | "REJECTED" +} diff --git a/service/app/src/entities/report/utils/formatReportDate.ts b/service/app/src/entities/report/utils/formatReportDate.ts new file mode 100644 index 00000000..b9abb91a --- /dev/null +++ b/service/app/src/entities/report/utils/formatReportDate.ts @@ -0,0 +1,7 @@ +export const formatReportDate = (isoString: string): string => { + const date = new Date(isoString) + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, "0") + const day = String(date.getDate()).padStart(2, "0") + return `${year}-${month}-${day}` +} diff --git a/service/app/src/entities/report/utils/formatReportNumber.ts b/service/app/src/entities/report/utils/formatReportNumber.ts new file mode 100644 index 00000000..d7b2678e --- /dev/null +++ b/service/app/src/entities/report/utils/formatReportNumber.ts @@ -0,0 +1,3 @@ +export const formatReportNumber = (num: number): string => { + return String(num).padStart(2, "0") +} diff --git a/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts b/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts new file mode 100644 index 00000000..0b29bd71 --- /dev/null +++ b/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts @@ -0,0 +1,27 @@ +import type { AdminReport, ReportedGame } from "../model/types" +import { formatReportDate } from "./formatReportDate" +import { formatReportNumber } from "./formatReportNumber" + +const STATUS_LABEL_MAP: Record = { + PENDING: "미처리", + RESOLVED: "삭제 완료", + REJECTED: "신고 무시", +} + +export const mapAdminReportToReportedGame = ( + report: AdminReport, + index: number, + page: number, + pageSize: number, +): ReportedGame => { + const number = page * pageSize + index + 1 + return { + id: String(report.reportId), + no: formatReportNumber(number), + title: report.gameName, + creatorName: report.creatorName, + reporterName: report.reporterName, + reportedAt: formatReportDate(report.reportedAt), + statusLabel: STATUS_LABEL_MAP[report.status], + } +} From 75aad17c4e1c8c81eeea14913cc242f919940013 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 17:16:34 +0900 Subject: [PATCH 18/36] feat(msw): add report detail handler --- service/app/src/mocks/handlers/admin.ts | 57 +++++++++++++++++++------ 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index 42003083..5298b9cc 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -2,7 +2,9 @@ import { http, HttpResponse } from "msw" import { type AdminReportsResponse, mockAdminReports } from "../data/admin" import { internalServerError, loginRequiredError } from "../data/common" +import { gameNotFoundError } from "../data/game" import { validateSessionCookie } from "../utils/gameHandlers" +import { generateMockQuestions } from "../utils/mockGenerators" import { generateSuccessResponse } from "../utils/responseHelpers" const MSW_BASE_URL = process.env.MSW_BASE_URL || "http://localhost:3000" @@ -22,20 +24,6 @@ export const adminHandlers = [ const pageParam = url.searchParams.get("page") const page = pageParam ? Math.max(0, parseInt(pageParam, 10)) : 0 - if (isNaN(page) || page < 0) { - return HttpResponse.json( - { - result: "ERROR", - data: null, - error: { - code: "E400", - message: "Invalid page parameter", - }, - }, - { status: 400 }, - ) - } - const totalPages = Math.ceil(TOTAL_ELEMENTS / PAGE_SIZE) const startIndex = page * PAGE_SIZE const endIndex = startIndex + PAGE_SIZE @@ -57,4 +45,45 @@ export const adminHandlers = [ return HttpResponse.json(internalServerError, { status: 500 }) } }), + http.get(`${MSW_BASE_URL}/admin/games/:reportId`, ({ request, params }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const reportId = Number(params.reportId) + + const report = mockAdminReports.find((r) => r.reportId === reportId) + if (!report) { + return HttpResponse.json(gameNotFoundError(reportId.toString()), { + status: 404, + }) + } + + const makerNickname = report.creatorName + const makerEmail = `${makerNickname.replace(/\s+/g, ".").toLowerCase()}@example.com` + const reporterNickname = report.reporterName + const reporterEmail = `${reporterNickname.replace(/\s+/g, ".").toLowerCase()}@example.com` + + const questionCount = 10 + const questions = generateMockQuestions(questionCount, 1) + + const data = { + gameTitle: report.gameName, + makerNickname, + makerEmail, + questionCount, + version: 1, + questions, + reporterEmail, + reporterNickname, + reasonCode: "VIOLENT_OR_DISTURBING_CONTENT", + } + + return HttpResponse.json(generateSuccessResponse(data)) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) + } + }), ] From 6422647160826271f6e0982ea10a47a05041d3fd Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 22:54:21 +0900 Subject: [PATCH 19/36] fix(msw): change endpoint --- service/app/src/entities/auth/api/validateSession.ts | 2 +- service/app/src/mocks/handlers/auth.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/app/src/entities/auth/api/validateSession.ts b/service/app/src/entities/auth/api/validateSession.ts index 0a4d918d..ad2bff44 100644 --- a/service/app/src/entities/auth/api/validateSession.ts +++ b/service/app/src/entities/auth/api/validateSession.ts @@ -4,6 +4,6 @@ import { UserSessionResponse } from "../model/auth" export const validateSession = async (): Promise => { - const response = await fetchClient.get("auth/me") + const response = await fetchClient.get("users/auth/me") return response.json() } diff --git a/service/app/src/mocks/handlers/auth.ts b/service/app/src/mocks/handlers/auth.ts index 89f5de20..9091ef6d 100644 --- a/service/app/src/mocks/handlers/auth.ts +++ b/service/app/src/mocks/handlers/auth.ts @@ -38,7 +38,7 @@ export const authHandlers = [ }, ) }), - http.get(`${MSW_BASE_URL}/auth/me`, async ({ request }) => { + http.get(`${MSW_BASE_URL}/users/auth/me`, async ({ request }) => { const cookieHeader = request.headers.get("Cookie") if (!validateSessionCookie(cookieHeader)) { return HttpResponse.json(loginRequiredError, { status: 401 }) From 65cdcd97341befe252d2a1e8bd858893dd89aa97 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 22:55:46 +0900 Subject: [PATCH 20/36] feat(admin): open preview on row click --- service/app/src/app/admin/reports/page.tsx | 9 ++++- .../game/hooks/useGameReportDetail.tsx | 39 +++++++++++++++++++ .../game/ui/GamePreview/gamePreviewAdmin.tsx | 33 ++++++++-------- .../entities/report/api/getReportDetail.ts | 10 +++++ .../app/src/entities/report/model/types.ts | 21 ++++++++++ .../shared/ui/admin-table/AdminTableRow.tsx | 12 ++++-- .../ui/ReportedGameRow.tsx | 8 +++- .../ui/ReportedGamesTable.tsx | 8 +++- 8 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 service/app/src/entities/game/hooks/useGameReportDetail.tsx create mode 100644 service/app/src/entities/report/api/getReportDetail.ts diff --git a/service/app/src/app/admin/reports/page.tsx b/service/app/src/app/admin/reports/page.tsx index bb0debb5..78799d36 100644 --- a/service/app/src/app/admin/reports/page.tsx +++ b/service/app/src/app/admin/reports/page.tsx @@ -3,6 +3,7 @@ import { Pagination } from "@ject-5-fe/design/components/pagination" import { useState } from "react" +import { useAdminGamePreview } from "@/entities/game/hooks/useGameReportDetail" import { useAdminReports } from "@/entities/report/hooks/useAdminReports" import { ReportedGamesTable } from "@/widgets/admin/reported-games-table" @@ -14,15 +15,21 @@ export default function AdminReportsPage() { page: pageIndex, }) + const { openAdminPreview } = useAdminGamePreview() + const handlePageChange = (page: number) => { setCurrentPage(page) } + const handleRowClick = (item: (typeof games)[number]) => { + openAdminPreview(item.id) + } + return (

신고접수

- +
{totalPages > 0 && ( diff --git a/service/app/src/entities/game/hooks/useGameReportDetail.tsx b/service/app/src/entities/game/hooks/useGameReportDetail.tsx new file mode 100644 index 00000000..0cc1fdca --- /dev/null +++ b/service/app/src/entities/game/hooks/useGameReportDetail.tsx @@ -0,0 +1,39 @@ +import { overlay } from "overlay-kit" +import { useCallback } from "react" + +import { REPORT_REASON_LABELS } from "@/entities/game/model/report" +import { GamePreviewAdmin } from "@/entities/game/ui/GamePreview" +import { getReportDetail } from "@/entities/report/api/getReportDetail" + +export const useAdminGamePreview = () => { + const openAdminPreview = useCallback(async (reportId: string | number) => { + const data = await getReportDetail(reportId) + + overlay.open(({ close, isOpen }) => ( + ({ + id: q.questionId.toString(), + title: q.questionText, + imageUrl: q.imageUrl, + }))} + reportData={{ + creator: { + name: data.makerNickname, + email: data.makerEmail, + }, + reporter: { + name: data.reporterNickname, + email: data.reporterEmail, + }, + category: REPORT_REASON_LABELS[data.reasonCode] ?? data.reasonCode, + }} + onClose={close} + isOpen={isOpen} + /> + )) + }, []) + + return { openAdminPreview } +} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx index 515d40fa..e7304668 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx @@ -51,15 +51,10 @@ export const GamePreviewAdmin = ({ return (
-
- - +
-
+
- - {reportData && ( - + - )} + + {reportData && ( + + )} +
diff --git a/service/app/src/entities/report/api/getReportDetail.ts b/service/app/src/entities/report/api/getReportDetail.ts new file mode 100644 index 00000000..4a8b2dc4 --- /dev/null +++ b/service/app/src/entities/report/api/getReportDetail.ts @@ -0,0 +1,10 @@ +import { fetchClient } from "@/shared/api/fetchClient" + +import type { ReportDetailData } from "../model/types" + +export const getReportDetail = async (reportId: string | number) => { + const response = await fetchClient.get( + `admin/games/${reportId}`, + ) + return response.json().then((res) => res.data) +} diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index 97270606..07211a98 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -1,3 +1,5 @@ +import type { ReportReasonCode } from "@/entities/game/model/report" + export interface ReportedGame { id: string no: number | string @@ -25,3 +27,22 @@ export interface AdminReport { reportedAt: string status: "PENDING" | "RESOLVED" | "REJECTED" } + +export interface ReportDetailData { + gameTitle: string + makerNickname: string + makerEmail: string + quetionCount: number + version: number + questions: { + questionId: number + questionOrder: number + imageUrl: string + questionText: string + questionAnswer: string + version: number + }[] + reporterEmail: string + reporterNickname: string + reasonCode: ReportReasonCode +} diff --git a/service/app/src/shared/ui/admin-table/AdminTableRow.tsx b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx index ca684d5d..dae77dd2 100644 --- a/service/app/src/shared/ui/admin-table/AdminTableRow.tsx +++ b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx @@ -1,11 +1,10 @@ import { cn } from "@ject-5-fe/design/utils/cn" -import type { ReactNode } from "react" +import type { HTMLAttributes, ReactNode } from "react" type AdminTableRowVariant = "header" | "data" -interface AdminTableRowProps { +interface AdminTableRowProps extends HTMLAttributes { children: ReactNode - className?: string variant?: AdminTableRowVariant } @@ -13,6 +12,7 @@ export const AdminTableRow = ({ children, className, variant = "data", + ...rest }: AdminTableRowProps) => { const base = "flex items-center py-20 min-w-0 px-16" const border = "border border-border-interactive-tertiary" @@ -21,5 +21,9 @@ export const AdminTableRow = ({ ? "bg-background-interactive-secondary-hovered" : "bg-transparent" - return
{children}
+ return ( +
+ {children} +
+ ) } diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx index a2e24aae..8bf3a1e0 100644 --- a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx @@ -7,11 +7,15 @@ import { interface ReportedGameRowProps { item: ReportedGame + onClick?: (item: ReportedGame) => void } -export const ReportedGameRow = ({ item }: ReportedGameRowProps) => { +export const ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => { return ( - + onClick?.(item)} + > {item.no} {item.title} diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx index 53d73334..1e5e9040 100644 --- a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx @@ -10,9 +10,13 @@ import { ReportedGameRow } from "./ReportedGameRow" interface ReportedGamesTableProps { items: ReportedGame[] + onRowClick?: (item: ReportedGame) => void } -export const ReportedGamesTable = ({ items }: ReportedGamesTableProps) => { +export const ReportedGamesTable = ({ + items, + onRowClick, +}: ReportedGamesTableProps) => { return ( @@ -37,7 +41,7 @@ export const ReportedGamesTable = ({ items }: ReportedGamesTableProps) => { {items.map((item) => ( - + ))} ) From ab4b5e3795e79fd2c8fdc1ddb5365acde46956d7 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Tue, 20 Jan 2026 23:43:16 +0900 Subject: [PATCH 21/36] feat(msw): add report delete handler --- .../app/src/entities/report/model/types.ts | 20 +++++++++++ service/app/src/mocks/handlers/admin.ts | 36 ++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index 07211a98..3b27e441 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -1,3 +1,5 @@ +import z from "zod" + import type { ReportReasonCode } from "@/entities/game/model/report" export interface ReportedGame { @@ -28,6 +30,24 @@ export interface AdminReport { status: "PENDING" | "RESOLVED" | "REJECTED" } +export const adminReportUpdateStatusSchema = z.enum([ + "IGNORE_REPORT", + "DELETE_GAME", +]) + +export type AdminReportUpdateStatus = z.infer< + typeof adminReportUpdateStatusSchema +> + +export const adminReportUpdateRequestSchema = z.object({ + status: adminReportUpdateStatusSchema, + reportId: z.number(), +}) + +export type AdminReportUpdateRequest = z.infer< + typeof adminReportUpdateRequestSchema +> + export interface ReportDetailData { gameTitle: string makerNickname: string diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index 5298b9cc..f25144ec 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -1,8 +1,14 @@ import { http, HttpResponse } from "msw" +import { adminReportUpdateRequestSchema } from "@/entities/report/model/types" + import { type AdminReportsResponse, mockAdminReports } from "../data/admin" import { internalServerError, loginRequiredError } from "../data/common" -import { gameNotFoundError } from "../data/game" +import { + gameMissingFieldsError, + gameNotFoundError, + gameSuccessResponse, +} from "../data/game" import { validateSessionCookie } from "../utils/gameHandlers" import { generateMockQuestions } from "../utils/mockGenerators" import { generateSuccessResponse } from "../utils/responseHelpers" @@ -86,4 +92,32 @@ export const adminHandlers = [ return HttpResponse.json(internalServerError, { status: 500 }) } }), + http.post(`${MSW_BASE_URL}/admin/games/delete`, async ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const json = await request.json() + const parsed = adminReportUpdateRequestSchema.safeParse(json) + + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } + + const { reportId } = parsed.data + + const report = mockAdminReports.find((r) => r.reportId === reportId) + if (!report) { + return HttpResponse.json(gameNotFoundError(reportId.toString()), { + status: 404, + }) + } + + return HttpResponse.json(gameSuccessResponse()) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) + } + }), ] From 0721e20c73dff11815058e296639443bd4ac9210 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Wed, 21 Jan 2026 01:37:27 +0900 Subject: [PATCH 22/36] feat(msw): add user block, delete game api --- .../ui/GamePreview/gamePreviewReportTable.tsx | 4 ++-- service/app/src/entities/report/api/index.ts | 2 ++ .../report/api/updateAdminReportStatus.ts | 13 +++++++++++ .../src/entities/suspension/api/blockUsers.ts | 11 ++++++++++ .../app/src/entities/suspension/api/index.ts | 1 + .../src/entities/suspension/model/types.ts | 22 +++++++++++++++++++ service/app/src/mocks/handlers/admin.ts | 20 +++++++++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 service/app/src/entities/report/api/updateAdminReportStatus.ts create mode 100644 service/app/src/entities/suspension/api/blockUsers.ts create mode 100644 service/app/src/entities/suspension/api/index.ts create mode 100644 service/app/src/entities/suspension/model/types.ts diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx index b53182d6..bb3b4eb3 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx @@ -25,7 +25,7 @@ export const GamePreviewReportTable = ({ 제작자
-
+
{creator.name}({creator.email}) @@ -46,7 +46,7 @@ export const GamePreviewReportTable = ({ 신고자
-
+
{reporter.name}({reporter.email}) diff --git a/service/app/src/entities/report/api/index.ts b/service/app/src/entities/report/api/index.ts index 88bccb5f..dcf16248 100644 --- a/service/app/src/entities/report/api/index.ts +++ b/service/app/src/entities/report/api/index.ts @@ -1 +1,3 @@ export * from "./getAdminReports" +export * from "./getReportDetail" +export * from "./updateAdminReportStatus" diff --git a/service/app/src/entities/report/api/updateAdminReportStatus.ts b/service/app/src/entities/report/api/updateAdminReportStatus.ts new file mode 100644 index 00000000..a21ff923 --- /dev/null +++ b/service/app/src/entities/report/api/updateAdminReportStatus.ts @@ -0,0 +1,13 @@ +import { fetchClient } from "@/shared/api/fetchClient" + +import type { AdminReportUpdateRequest } from "../model/types" + +export const updateAdminReportStatus = async ( + payload: AdminReportUpdateRequest, +) => { + const response = await fetchClient.post("admin/games/delete", { + json: payload, + }) + + return response.json() +} diff --git a/service/app/src/entities/suspension/api/blockUsers.ts b/service/app/src/entities/suspension/api/blockUsers.ts new file mode 100644 index 00000000..3847b31e --- /dev/null +++ b/service/app/src/entities/suspension/api/blockUsers.ts @@ -0,0 +1,11 @@ +import { fetchClient } from "@/shared/api/fetchClient" + +import type { BlockUsersRequest } from "../model/types" + +export const blockUsers = async (payload: BlockUsersRequest) => { + const response = await fetchClient.post("admin/users/block", { + json: payload, + }) + + return response.json() +} diff --git a/service/app/src/entities/suspension/api/index.ts b/service/app/src/entities/suspension/api/index.ts new file mode 100644 index 00000000..26352323 --- /dev/null +++ b/service/app/src/entities/suspension/api/index.ts @@ -0,0 +1 @@ +export * from "./blockUsers" diff --git a/service/app/src/entities/suspension/model/types.ts b/service/app/src/entities/suspension/model/types.ts new file mode 100644 index 00000000..c53b6cf8 --- /dev/null +++ b/service/app/src/entities/suspension/model/types.ts @@ -0,0 +1,22 @@ +import z from "zod" + +export interface SuspendedUser { + id: string + name: string + email: string + reason: string + suspendedAt: string + blockStatusLabel?: string +} + +export const banListItemSchema = z.object({ + email: z.string().min(1), + reason: z.string().optional(), +}) + +export const blockUsersRequestSchema = z.object({ + banList: z.array(banListItemSchema).min(1), +}) + +export type BanListItem = z.infer +export type BlockUsersRequest = z.infer diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index f25144ec..934857fe 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -1,6 +1,7 @@ import { http, HttpResponse } from "msw" import { adminReportUpdateRequestSchema } from "@/entities/report/model/types" +import { blockUsersRequestSchema } from "@/entities/suspension/model/types" import { type AdminReportsResponse, mockAdminReports } from "../data/admin" import { internalServerError, loginRequiredError } from "../data/common" @@ -115,6 +116,25 @@ export const adminHandlers = [ }) } + return HttpResponse.json(gameSuccessResponse()) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) + } + }), + http.post(`${MSW_BASE_URL}/admin/users/block`, async ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const json = await request.json() + const parsed = blockUsersRequestSchema.safeParse(json) + + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } + return HttpResponse.json(gameSuccessResponse()) } catch { return HttpResponse.json(internalServerError, { status: 500 }) From 575f22b0a9db9d75a111e7d01366979f713f91fe Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Wed, 21 Jan 2026 15:05:52 +0900 Subject: [PATCH 23/36] feat(admin): match api call on admin game preview --- .../game/hooks/useGameReportDetail.tsx | 88 +++++++++++++------ .../game/ui/GamePreview/gamePreviewAdmin.tsx | 2 +- .../GamePreview/gamePreviewQuestionList.tsx | 4 +- .../report/hooks/useAdminReportMutations.ts | 40 +++++++++ .../app/src/entities/report/model/types.ts | 8 +- service/app/src/mocks/data/admin.ts | 4 +- service/app/src/mocks/handlers/admin.ts | 15 +++- 7 files changed, 123 insertions(+), 38 deletions(-) create mode 100644 service/app/src/entities/report/hooks/useAdminReportMutations.ts diff --git a/service/app/src/entities/game/hooks/useGameReportDetail.tsx b/service/app/src/entities/game/hooks/useGameReportDetail.tsx index 0cc1fdca..2ba39c55 100644 --- a/service/app/src/entities/game/hooks/useGameReportDetail.tsx +++ b/service/app/src/entities/game/hooks/useGameReportDetail.tsx @@ -4,36 +4,68 @@ import { useCallback } from "react" import { REPORT_REASON_LABELS } from "@/entities/game/model/report" import { GamePreviewAdmin } from "@/entities/game/ui/GamePreview" import { getReportDetail } from "@/entities/report/api/getReportDetail" +import { useAdminReportMutations } from "@/entities/report/hooks/useAdminReportMutations" export const useAdminGamePreview = () => { - const openAdminPreview = useCallback(async (reportId: string | number) => { - const data = await getReportDetail(reportId) - - overlay.open(({ close, isOpen }) => ( - ({ - id: q.questionId.toString(), - title: q.questionText, - imageUrl: q.imageUrl, - }))} - reportData={{ - creator: { - name: data.makerNickname, - email: data.makerEmail, - }, - reporter: { - name: data.reporterNickname, - email: data.reporterEmail, - }, - category: REPORT_REASON_LABELS[data.reasonCode] ?? data.reasonCode, - }} - onClose={close} - isOpen={isOpen} - /> - )) - }, []) + const { ignoreReportMutation, deleteGameMutation, blockUserMutation } = + useAdminReportMutations() + + const openAdminPreview = useCallback( + async (reportId: string) => { + const data = await getReportDetail(reportId) + + overlay.open(({ close, isOpen }) => { + const handleIgnoreReport = async () => { + await ignoreReportMutation.mutateAsync(reportId) + close() + } + + const handleDeleteGame = async () => { + await deleteGameMutation.mutateAsync(reportId) + close() + } + + const handleBlockCreator = async () => { + await blockUserMutation.mutateAsync(data.makerEmail) + } + + const handleBlockReporter = async () => { + await blockUserMutation.mutateAsync(data.reporterEmail) + } + + return ( + ({ + id: q.questionId.toString(), + title: q.questionText, + imageUrl: q.imageUrl, + }))} + reportData={{ + creator: { + name: data.makerNickname, + email: data.makerEmail, + }, + reporter: { + name: data.reporterNickname, + email: data.reporterEmail, + }, + category: + REPORT_REASON_LABELS[data.reasonCode] ?? data.reasonCode, + }} + onClose={close} + isOpen={isOpen} + onIgnoreReport={handleIgnoreReport} + onDeleteGame={handleDeleteGame} + onBlockCreator={handleBlockCreator} + onBlockReporter={handleBlockReporter} + /> + ) + }) + }, + [ignoreReportMutation, deleteGameMutation, blockUserMutation], + ) return { openAdminPreview } } diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx index e7304668..8f8e4c7a 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx @@ -54,7 +54,7 @@ export const GamePreviewAdmin = ({
-
+
{ return ( -
+
{questions.map((question) => ( diff --git a/service/app/src/entities/report/hooks/useAdminReportMutations.ts b/service/app/src/entities/report/hooks/useAdminReportMutations.ts new file mode 100644 index 00000000..f9ca43c9 --- /dev/null +++ b/service/app/src/entities/report/hooks/useAdminReportMutations.ts @@ -0,0 +1,40 @@ +import { useMutation } from "@tanstack/react-query" + +import { blockUsers } from "@/entities/suspension/api/blockUsers" +import { queryClient } from "@/shared/lib/queryClient" + +import { updateAdminReportStatus } from "../api/updateAdminReportStatus" + +export const useAdminReportMutations = () => { + const ignoreReportMutation = useMutation({ + mutationFn: (reportId: string) => + updateAdminReportStatus({ status: "IGNORE_REPORT", reportId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["adminReports"] }) + }, + }) + + const deleteGameMutation = useMutation({ + mutationFn: (reportId: string) => + updateAdminReportStatus({ status: "DELETE_GAME", reportId }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["adminReports"] }) + }, + }) + + const blockUserMutation = useMutation({ + mutationFn: (email: string) => + blockUsers({ + banList: [{ email }], + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["adminReports"] }) + }, + }) + + return { + ignoreReportMutation, + deleteGameMutation, + blockUserMutation, + } +} diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index 3b27e441..a23401df 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -41,13 +41,19 @@ export type AdminReportUpdateStatus = z.infer< export const adminReportUpdateRequestSchema = z.object({ status: adminReportUpdateStatusSchema, - reportId: z.number(), + reportId: z.string(), }) export type AdminReportUpdateRequest = z.infer< typeof adminReportUpdateRequestSchema > +export const reportIdParamsSchema = z.object({ + reportId: z.string(), +}) + +export type ReportIdParams = z.infer + export interface ReportDetailData { gameTitle: string makerNickname: string diff --git a/service/app/src/mocks/data/admin.ts b/service/app/src/mocks/data/admin.ts index 1314978c..eabf976e 100644 --- a/service/app/src/mocks/data/admin.ts +++ b/service/app/src/mocks/data/admin.ts @@ -1,7 +1,7 @@ export type ReportStatus = "PENDING" | "RESOLVED" | "REJECTED" export interface AdminReport { - reportId: number + reportId: string gameName: string creatorName: string reporterName: string @@ -86,7 +86,7 @@ export const generateMockAdminReports = ( date.setDate(date.getDate() - daysAgo) result.push({ - reportId: i + 1, + reportId: (i + 1).toString(), gameName: gameNames[randomIndex], creatorName: creatorNames[creatorIndex], reporterName: reporterNames[reporterIndex], diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index 934857fe..f72c9323 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -1,6 +1,9 @@ import { http, HttpResponse } from "msw" -import { adminReportUpdateRequestSchema } from "@/entities/report/model/types" +import { + adminReportUpdateRequestSchema, + reportIdParamsSchema, +} from "@/entities/report/model/types" import { blockUsersRequestSchema } from "@/entities/suspension/model/types" import { type AdminReportsResponse, mockAdminReports } from "../data/admin" @@ -59,11 +62,15 @@ export const adminHandlers = [ return HttpResponse.json(loginRequiredError, { status: 401 }) } - const reportId = Number(params.reportId) + const parsed = reportIdParamsSchema.safeParse(params) + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } + const { reportId } = parsed.data const report = mockAdminReports.find((r) => r.reportId === reportId) if (!report) { - return HttpResponse.json(gameNotFoundError(reportId.toString()), { + return HttpResponse.json(gameNotFoundError(reportId), { status: 404, }) } @@ -111,7 +118,7 @@ export const adminHandlers = [ const report = mockAdminReports.find((r) => r.reportId === reportId) if (!report) { - return HttpResponse.json(gameNotFoundError(reportId.toString()), { + return HttpResponse.json(gameNotFoundError(reportId), { status: 404, }) } From 234b7ebceaf612b915cbd414ccfdd1c1893949b0 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Wed, 21 Jan 2026 15:33:47 +0900 Subject: [PATCH 24/36] fix(admin): fix report id type to number, fix enum type for report --- .../entities/game/hooks/useGamePreview.tsx | 2 +- .../game/hooks/useGameReportDetail.tsx | 4 ++-- .../game/ui/GamePreview/gamePreviewAdmin.tsx | 2 +- .../GamePreview/gamePreviewQuestionList.tsx | 2 +- .../game/ui/GamePreview/gamePreviewUser.tsx | 2 +- .../entities/report/api/getReportDetail.ts | 2 +- .../report/hooks/useAdminReportMutations.ts | 4 ++-- .../entities/report/hooks/useAdminReports.ts | 4 +--- .../app/src/entities/report/model/types.ts | 9 ++++--- .../utils/mapAdminReportToReportedGame.ts | 12 +++------- service/app/src/mocks/data/admin.ts | 24 +++---------------- service/app/src/mocks/data/game.ts | 4 ++++ service/app/src/mocks/handlers/admin.ts | 9 +++---- .../app/src/mocks/utils/responseHelpers.ts | 7 ++++++ .../ui/ReportedGameRow.tsx | 2 +- 15 files changed, 37 insertions(+), 52 deletions(-) diff --git a/service/app/src/entities/game/hooks/useGamePreview.tsx b/service/app/src/entities/game/hooks/useGamePreview.tsx index 6c4962aa..5f185ceb 100644 --- a/service/app/src/entities/game/hooks/useGamePreview.tsx +++ b/service/app/src/entities/game/hooks/useGamePreview.tsx @@ -34,7 +34,7 @@ export const useGamePreview = ({ onError }: UseGamePreviewParams = {}) => { creatorName={gameDetail.nickname} questionCount={gameDetail.questionCount} questions={gameDetail.questions.map((question: GameQuestion) => ({ - id: question.questionId.toString(), + id: question.questionId, title: question.questionText, imageUrl: question.imageUrl, }))} diff --git a/service/app/src/entities/game/hooks/useGameReportDetail.tsx b/service/app/src/entities/game/hooks/useGameReportDetail.tsx index 2ba39c55..3e4c1618 100644 --- a/service/app/src/entities/game/hooks/useGameReportDetail.tsx +++ b/service/app/src/entities/game/hooks/useGameReportDetail.tsx @@ -11,7 +11,7 @@ export const useAdminGamePreview = () => { useAdminReportMutations() const openAdminPreview = useCallback( - async (reportId: string) => { + async (reportId: number) => { const data = await getReportDetail(reportId) overlay.open(({ close, isOpen }) => { @@ -38,7 +38,7 @@ export const useAdminGamePreview = () => { gameTitle={data.gameTitle} questionCount={data.quetionCount} questions={data.questions.map((q) => ({ - id: q.questionId.toString(), + id: q.questionId, title: q.questionText, imageUrl: q.imageUrl, }))} diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx index 8f8e4c7a..134308e5 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx @@ -12,7 +12,7 @@ interface GamePreviewAdminProps { gameTitle?: string questionCount?: number questions?: Array<{ - id: string + id: number title: string imageUrl?: string }> diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx index b77dca91..fbe42330 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewQuestionList.tsx @@ -5,7 +5,7 @@ import * as GameCard from "@/entities/game/ui/GameCard/gameCard" interface GamePreviewQuestionListProps { questionCount: number questions: Array<{ - id: string + id: number title: string imageUrl?: string }> diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx index 9e865fe1..50988659 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx @@ -13,7 +13,7 @@ interface GamePreviewUserProps { creatorName?: string questionCount?: number questions?: Array<{ - id: string + id: number title: string imageUrl?: string }> diff --git a/service/app/src/entities/report/api/getReportDetail.ts b/service/app/src/entities/report/api/getReportDetail.ts index 4a8b2dc4..a79899b8 100644 --- a/service/app/src/entities/report/api/getReportDetail.ts +++ b/service/app/src/entities/report/api/getReportDetail.ts @@ -2,7 +2,7 @@ import { fetchClient } from "@/shared/api/fetchClient" import type { ReportDetailData } from "../model/types" -export const getReportDetail = async (reportId: string | number) => { +export const getReportDetail = async (reportId: number) => { const response = await fetchClient.get( `admin/games/${reportId}`, ) diff --git a/service/app/src/entities/report/hooks/useAdminReportMutations.ts b/service/app/src/entities/report/hooks/useAdminReportMutations.ts index f9ca43c9..fa7d2958 100644 --- a/service/app/src/entities/report/hooks/useAdminReportMutations.ts +++ b/service/app/src/entities/report/hooks/useAdminReportMutations.ts @@ -7,7 +7,7 @@ import { updateAdminReportStatus } from "../api/updateAdminReportStatus" export const useAdminReportMutations = () => { const ignoreReportMutation = useMutation({ - mutationFn: (reportId: string) => + mutationFn: (reportId: number) => updateAdminReportStatus({ status: "IGNORE_REPORT", reportId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["adminReports"] }) @@ -15,7 +15,7 @@ export const useAdminReportMutations = () => { }) const deleteGameMutation = useMutation({ - mutationFn: (reportId: string) => + mutationFn: (reportId: number) => updateAdminReportStatus({ status: "DELETE_GAME", reportId }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["adminReports"] }) diff --git a/service/app/src/entities/report/hooks/useAdminReports.ts b/service/app/src/entities/report/hooks/useAdminReports.ts index 23647f3f..3258a490 100644 --- a/service/app/src/entities/report/hooks/useAdminReports.ts +++ b/service/app/src/entities/report/hooks/useAdminReports.ts @@ -34,9 +34,7 @@ export const useAdminReports = ({ }) const games: ReportedGame[] = - data?.report.map((report, index) => - mapAdminReportToReportedGame(report, index, page, data.size), - ) ?? [] + data?.report.map((report) => mapAdminReportToReportedGame(report)) ?? [] return { games, diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index a23401df..5cc432c3 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -3,8 +3,7 @@ import z from "zod" import type { ReportReasonCode } from "@/entities/game/model/report" export interface ReportedGame { - id: string - no: number | string + id: number title: string creatorName: string reporterName: string @@ -27,7 +26,7 @@ export interface AdminReport { creatorName: string reporterName: string reportedAt: string - status: "PENDING" | "RESOLVED" | "REJECTED" + status: "PENDING" | "GAME_DELETED" | "IGNORED" } export const adminReportUpdateStatusSchema = z.enum([ @@ -41,7 +40,7 @@ export type AdminReportUpdateStatus = z.infer< export const adminReportUpdateRequestSchema = z.object({ status: adminReportUpdateStatusSchema, - reportId: z.string(), + reportId: z.coerce.number(), }) export type AdminReportUpdateRequest = z.infer< @@ -49,7 +48,7 @@ export type AdminReportUpdateRequest = z.infer< > export const reportIdParamsSchema = z.object({ - reportId: z.string(), + reportId: z.coerce.number(), }) export type ReportIdParams = z.infer diff --git a/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts b/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts index 0b29bd71..f353cb17 100644 --- a/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts +++ b/service/app/src/entities/report/utils/mapAdminReportToReportedGame.ts @@ -1,23 +1,17 @@ import type { AdminReport, ReportedGame } from "../model/types" import { formatReportDate } from "./formatReportDate" -import { formatReportNumber } from "./formatReportNumber" const STATUS_LABEL_MAP: Record = { PENDING: "미처리", - RESOLVED: "삭제 완료", - REJECTED: "신고 무시", + GAME_DELETED: "삭제 완료", + IGNORED: "신고 무시", } export const mapAdminReportToReportedGame = ( report: AdminReport, - index: number, - page: number, - pageSize: number, ): ReportedGame => { - const number = page * pageSize + index + 1 return { - id: String(report.reportId), - no: formatReportNumber(number), + id: report.reportId, title: report.gameName, creatorName: report.creatorName, reporterName: report.reporterName, diff --git a/service/app/src/mocks/data/admin.ts b/service/app/src/mocks/data/admin.ts index eabf976e..783462be 100644 --- a/service/app/src/mocks/data/admin.ts +++ b/service/app/src/mocks/data/admin.ts @@ -1,22 +1,4 @@ -export type ReportStatus = "PENDING" | "RESOLVED" | "REJECTED" - -export interface AdminReport { - reportId: string - gameName: string - creatorName: string - reporterName: string - reportedAt: string - status: ReportStatus -} - -export interface AdminReportsResponse { - report: AdminReport[] - page: number - size: number - totalElements: number - totalPages: number - hasNext: boolean -} +import type { AdminReport } from "@/entities/report/model/types" const gameNames = [ "퀴즈 마스터", @@ -57,7 +39,7 @@ const reporterNames = [ "신고자10", ] -const statuses: ReportStatus[] = ["PENDING", "RESOLVED", "REJECTED"] +const statuses: AdminReport["status"][] = ["PENDING", "GAME_DELETED", "IGNORED"] export const generateMockAdminReports = ( count: number, @@ -86,7 +68,7 @@ export const generateMockAdminReports = ( date.setDate(date.getDate() - daysAgo) result.push({ - reportId: (i + 1).toString(), + reportId: i + 1, gameName: gameNames[randomIndex], creatorName: creatorNames[creatorIndex], reporterName: reporterNames[reporterIndex], diff --git a/service/app/src/mocks/data/game.ts b/service/app/src/mocks/data/game.ts index 8ca937a0..7513d488 100644 --- a/service/app/src/mocks/data/game.ts +++ b/service/app/src/mocks/data/game.ts @@ -8,6 +8,7 @@ import { UUID } from "@/shared/api/types/common" import { generatePresignedUrlData } from "../utils/mockGenerators" import { generateGameErrorResponse, + generateReportErrorResponse, generateSuccessResponse, } from "../utils/responseHelpers" import { mockGameList } from "./common" @@ -53,6 +54,9 @@ export const gameDetailSuccess = (gameDetailData: GameDetailData) => ({ export const gameNotFoundError = (gameId: UUID) => generateGameErrorResponse.notFound(gameId) +export const reportNotFoundError = (reportId: number) => + generateReportErrorResponse.notFound(reportId) + export const gameUnauthorizedError = () => generateGameErrorResponse.unauthorized() diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index f72c9323..2541f299 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -1,17 +1,18 @@ import { http, HttpResponse } from "msw" import { + AdminReportsResponse, adminReportUpdateRequestSchema, reportIdParamsSchema, } from "@/entities/report/model/types" import { blockUsersRequestSchema } from "@/entities/suspension/model/types" -import { type AdminReportsResponse, mockAdminReports } from "../data/admin" +import { mockAdminReports } from "../data/admin" import { internalServerError, loginRequiredError } from "../data/common" import { gameMissingFieldsError, - gameNotFoundError, gameSuccessResponse, + reportNotFoundError, } from "../data/game" import { validateSessionCookie } from "../utils/gameHandlers" import { generateMockQuestions } from "../utils/mockGenerators" @@ -70,7 +71,7 @@ export const adminHandlers = [ const { reportId } = parsed.data const report = mockAdminReports.find((r) => r.reportId === reportId) if (!report) { - return HttpResponse.json(gameNotFoundError(reportId), { + return HttpResponse.json(reportNotFoundError(reportId), { status: 404, }) } @@ -118,7 +119,7 @@ export const adminHandlers = [ const report = mockAdminReports.find((r) => r.reportId === reportId) if (!report) { - return HttpResponse.json(gameNotFoundError(reportId), { + return HttpResponse.json(reportNotFoundError(reportId), { status: 404, }) } diff --git a/service/app/src/mocks/utils/responseHelpers.ts b/service/app/src/mocks/utils/responseHelpers.ts index f140ec70..a4166e71 100644 --- a/service/app/src/mocks/utils/responseHelpers.ts +++ b/service/app/src/mocks/utils/responseHelpers.ts @@ -39,3 +39,10 @@ export const generateGameErrorResponse = { gameId, }), } + +export const generateReportErrorResponse = { + notFound: (reportId: number) => + generateErrorResponse("E404", "해당 신고가 존재하지 않습니다.", { + reportId, + }), +} diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx index 8bf3a1e0..f67686c7 100644 --- a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx @@ -17,7 +17,7 @@ export const ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => { onClick={() => onClick?.(item)} > - {item.no} + {item.id} {item.title} From d1a0b8a28378853ee37b9008cd28abaeeed0a360 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Wed, 21 Jan 2026 16:01:39 +0900 Subject: [PATCH 25/36] fix(admin): move directory --- .../admin/users/_components/{ => dialog}/blockConfirmModal.tsx | 0 .../admin/users/_components/{ => dialog}/unblockConfirmModal.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename service/app/src/app/admin/users/_components/{ => dialog}/blockConfirmModal.tsx (100%) rename service/app/src/app/admin/users/_components/{ => dialog}/unblockConfirmModal.tsx (100%) diff --git a/service/app/src/app/admin/users/_components/blockConfirmModal.tsx b/service/app/src/app/admin/users/_components/dialog/blockConfirmModal.tsx similarity index 100% rename from service/app/src/app/admin/users/_components/blockConfirmModal.tsx rename to service/app/src/app/admin/users/_components/dialog/blockConfirmModal.tsx diff --git a/service/app/src/app/admin/users/_components/unblockConfirmModal.tsx b/service/app/src/app/admin/users/_components/dialog/unblockConfirmModal.tsx similarity index 100% rename from service/app/src/app/admin/users/_components/unblockConfirmModal.tsx rename to service/app/src/app/admin/users/_components/dialog/unblockConfirmModal.tsx From 4354cb9dd50ccbe5181f0f74d15402357790856c Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:31:47 +0900 Subject: [PATCH 26/36] feat(reports): add a11y tags --- .../app/admin/_components/adminNavigation.tsx | 11 +++- .../ui/GamePreview/gamePreviewReportTable.tsx | 51 +++++++++++++++---- .../shared/ui/admin-table/AdminTableRoot.tsx | 8 ++- .../shared/ui/admin-table/AdminTableRow.tsx | 2 +- .../shared/ui/admin-table/AdminTableText.tsx | 1 + .../ui/ReportedGameRow.tsx | 2 + .../ui/ReportedGamesTable.tsx | 8 ++- 7 files changed, 68 insertions(+), 15 deletions(-) diff --git a/service/app/src/app/admin/_components/adminNavigation.tsx b/service/app/src/app/admin/_components/adminNavigation.tsx index acdc8db2..252807d3 100644 --- a/service/app/src/app/admin/_components/adminNavigation.tsx +++ b/service/app/src/app/admin/_components/adminNavigation.tsx @@ -12,6 +12,8 @@ export const AdminNavigation = () => { const isActive = (href: string) => pathname === href || pathname.startsWith(`${href}/`) + const getAriaCurrent = (active: boolean) => (active ? "page" : undefined) + const navItemClass = (active: boolean) => cn( "typography-heading-md-semibold", @@ -30,22 +32,29 @@ export const AdminNavigation = () => {
} rightContent={ -
+
신고접수 게임관리 회원관리 diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx index bb3b4eb3..1cb74cb3 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewReportTable.tsx @@ -3,8 +3,8 @@ import { PrimaryBoxButton } from "@ject-5-fe/design/components/button" interface GamePreviewReportTableProps { - creator: { name: string; email: string } - reporter: { name: string; email: string } + creator: { name: string; email: string; isBlocked: boolean } + reporter: { name: string; email: string; isBlocked: boolean } category: string onBlockCreator?: () => void onBlockReporter?: () => void @@ -18,14 +18,25 @@ export const GamePreviewReportTable = ({ onBlockReporter, }: GamePreviewReportTableProps) => { return ( -
+
-
+
제작자
-
+
{creator.name}({creator.email}) @@ -34,6 +45,8 @@ export const GamePreviewReportTable = ({ _style="solid" onClick={onBlockCreator} className="h-[44px] w-[83px] bg-background-interactive-destructive" + aria-label={`제작자 ${creator.name} 차단`} + disabled={creator.isBlocked} > 차단 @@ -41,12 +54,19 @@ export const GamePreviewReportTable = ({
-
+
신고자
-
+
{reporter.name}({reporter.email}) @@ -55,6 +75,8 @@ export const GamePreviewReportTable = ({ _style="solid" onClick={onBlockReporter} className="h-[44px] w-[83px] bg-background-interactive-destructive" + aria-label={`신고자 ${reporter.name} 차단`} + disabled={reporter.isBlocked} > 차단 @@ -62,13 +84,22 @@ export const GamePreviewReportTable = ({
-
+
항목
-
- +
+ {category}
diff --git a/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx b/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx index 8389f581..3cd6ab69 100644 --- a/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx +++ b/service/app/src/shared/ui/admin-table/AdminTableRoot.tsx @@ -4,14 +4,20 @@ import type { ReactNode } from "react" interface AdminTableRootProps { children: ReactNode className?: string + ariaLabel?: string } export const AdminTableRoot = ({ children, className, + ariaLabel, }: AdminTableRootProps) => { return ( -
+
{children}
diff --git a/service/app/src/shared/ui/admin-table/AdminTableRow.tsx b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx index dae77dd2..aad5d3d2 100644 --- a/service/app/src/shared/ui/admin-table/AdminTableRow.tsx +++ b/service/app/src/shared/ui/admin-table/AdminTableRow.tsx @@ -22,7 +22,7 @@ export const AdminTableRow = ({ : "bg-transparent" return ( -
+
{children}
) diff --git a/service/app/src/shared/ui/admin-table/AdminTableText.tsx b/service/app/src/shared/ui/admin-table/AdminTableText.tsx index 5a778ac3..87528f9c 100644 --- a/service/app/src/shared/ui/admin-table/AdminTableText.tsx +++ b/service/app/src/shared/ui/admin-table/AdminTableText.tsx @@ -25,6 +25,7 @@ export const AdminTableText = ({ tone === "header" ? header : body, className, )} + role={tone === "header" ? "columnheader" : "cell"} > {children} diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx index f67686c7..0d9bcba9 100644 --- a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGameRow.tsx @@ -15,6 +15,8 @@ export const ReportedGameRow = ({ item, onClick }: ReportedGameRowProps) => { onClick?.(item)} + aria-label={`게임명 ${item.title} 제작자 ${item.creatorName} 신고자 ${item.reporterName} 신고일자 ${item.reportedAt} 처리 여부 ${item.statusLabel}`} + tabIndex={0} > {item.id} diff --git a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx index 1e5e9040..e96f09d4 100644 --- a/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx +++ b/service/app/src/widgets/admin/reported-games-table/ui/ReportedGamesTable.tsx @@ -18,8 +18,12 @@ export const ReportedGamesTable = ({ onRowClick, }: ReportedGamesTableProps) => { return ( - - + + 번호 From 02d8496b4257fbf78bd1fe206e9a17616948cac5 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:32:52 +0900 Subject: [PATCH 27/36] test(reports): add e2e tests for reports page --- .../admin/reports/__tests__/reports.spec.ts | 198 ++++++++++++++++++ .../app/admin/reports/__tests__/reportsPOM.ts | 160 ++++++++++++++ 2 files changed, 358 insertions(+) create mode 100644 service/app/src/app/admin/reports/__tests__/reports.spec.ts create mode 100644 service/app/src/app/admin/reports/__tests__/reportsPOM.ts diff --git a/service/app/src/app/admin/reports/__tests__/reports.spec.ts b/service/app/src/app/admin/reports/__tests__/reports.spec.ts new file mode 100644 index 00000000..5a9df4af --- /dev/null +++ b/service/app/src/app/admin/reports/__tests__/reports.spec.ts @@ -0,0 +1,198 @@ +import { expect, test } from "@playwright/test" + +import { ReportsPOM } from "./reportsPOM" + +test.describe("신고접수 페이지", () => { + test.describe("인증 및 권한", () => { + test("비로그인 사용자는 홈으로 리다이렉트 되어야 한다", async ({ + page, + }) => { + await page.goto("/admin/reports") + await page.waitForURL("/") + }) + + // test("로그인 사용자라도 ADMIN이 아닌 경우 홈으로 리다이렉트 되어야 한다", async ({ + // page, + // }) => { + // // TODO: USER role로 로그인 상태 설정 필요 + // await page.goto("/admin/reports") + // await page.waitForURL("/") + // }) + }) + + test.describe("초기 UI 요소", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("초기 UI 요소가 잘 표시되어야 한다", async () => { + await pom.checkNavigationHighlight() + await pom.checkReportedTable() + }) + }) + + test.describe("네비게이션", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("게임 관리 클릭시 게임 관리 페이지로 이동한다", async ({ page }) => { + await pom.clickGamesTab() + await page.waitForURL("/admin/games") + }) + + test("회원 관리 클릭시 회원 관리 페이지로 이동한다", async ({ page }) => { + await pom.clickUsersTab() + await page.waitForURL("/admin/users") + }) + }) + + test.describe("기본 리스트 렌더링", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("한 화면에 표시된 row 수가 7개 이하이다", async () => { + await pom.checkRowCount() + }) + + test("각 row에 컬럼이 존재한다 (번호, 게임명, 제작자, 신고자, 신고일자, 처리여부)", async () => { + await pom.checkRowColumns() + }) + }) + + test.describe("신고 상세 팝업: 공통", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("신고 항목을 클릭하면 신고 내용 확인 팝업이 표시된다", async () => { + await pom.clickReportedGameRow(0) + await pom.checkReportDetailPopup() + }) + + test("게임 정보가 표시된다 (게임명, 전체 문제 수, 신고 사유)", async () => { + await pom.clickReportedGameRow(0) + await pom.checkGameInfo() + }) + + test("팝업 닫기 버튼 클릭시 팝업이 닫힌다", async () => { + await pom.clickReportedGameRow(0) + await pom.checkReportDetailPopup() + await pom.clickCloseButton() + await expect(pom.reportDetail).not.toBeVisible() + }) + }) + + test.describe("신고 상세 팝업: 삭제되지 않은 게임", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("문제 목록이 캐러셀로 노출된다", async () => { + await pom.clickReportedGameRow(0) + await pom.checkCarousel() + }) + + test("삭제되지 않은 게임에서 게임 삭제 클릭시 팝업이 닫히고 상태가 삭제 완료로 업데이트된다", async () => { + await pom.clickReportedGameRow(0) + + await pom.clickDeleteGame() + await expect(pom.reportDetail).not.toBeVisible() + + const updatedAriaLabel = await pom.reportedGameRows + .nth(1) + .getAttribute("aria-label") + expect(updatedAriaLabel).toContain("처리 여부 삭제 완료") + }) + }) + + test.describe("신고 상세 팝업: 삭제된 게임", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + test("삭제된 게임의 경우 삭제 처리된 게임입니다 문구가 표시되고 캐러셀이 노출되지 않는다", async () => { + await pom.clickReportedGameRow(1) + + await pom.checkDeletedGameMessage() + await expect(pom.gameQuestionList).not.toBeVisible() + }) + + test("삭제된 게임 클릭시 신고 무시 버튼과 게임 삭제 버튼이 비활성화된다", async () => { + await pom.clickReportedGameRow(1) + + await pom.checkButtonDisabled(pom.reportIgnoreButton) + await pom.checkButtonDisabled(pom.gameDeleteButton) + }) + }) + + test.describe("신고 무시", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("신고 무시 클릭시 팝업이 닫히고 상태가 신고 무시로 업데이트된다", async () => { + await pom.clickReportedGameRow(0) + const row = pom.reportedGameRows.first() + + await pom.clickIgnoreReport() + await expect(pom.reportDetail).not.toBeVisible() + + const updatedAriaLabel = await row.getAttribute("aria-label") + expect(updatedAriaLabel).toContain("처리 여부 신고 무시") + }) + }) + + test.describe("사용자 차단", () => { + test.use({ storageState: "playwright/.auth/user.json" }) + + let pom: ReportsPOM + + test.beforeEach(async ({ page }) => { + pom = new ReportsPOM(page) + await pom.navigateToReports() + }) + + test("사용자 차단 클릭시 사용자 차단 버튼이 비활성화된다", async () => { + await pom.clickReportedGameRow(0) + await pom.clickBlockCreator() + await expect(pom.reportDetail).toBeVisible() + await pom.checkButtonDisabled(pom.blockCreatorButton) + }) + }) +}) diff --git a/service/app/src/app/admin/reports/__tests__/reportsPOM.ts b/service/app/src/app/admin/reports/__tests__/reportsPOM.ts new file mode 100644 index 00000000..4b8fa844 --- /dev/null +++ b/service/app/src/app/admin/reports/__tests__/reportsPOM.ts @@ -0,0 +1,160 @@ +import { expect, type Locator, type Page } from "@playwright/test" + +export class ReportsPOM { + readonly page: Page + + // 네비게이션 영역 + readonly reportsTab: Locator + readonly gamesTab: Locator + readonly usersTab: Locator + + // 메인 콘텐츠 영역 + readonly reportsTable: Locator + readonly reportedGameRows: Locator + + // 팝업 + readonly reportDetail: Locator + readonly reportedGameTitle: Locator + readonly reportDetailCloseButton: Locator + readonly reportIgnoreButton: Locator + readonly gameDeleteButton: Locator + readonly gameQuestionList: Locator + readonly gameQuestionCount: Locator + readonly gameReportTable: Locator + readonly gameCreatorInfo: Locator + readonly gameReporterInfo: Locator + readonly reportReasonInfo: Locator + readonly blockCreatorButton: Locator + readonly blockReporterButton: Locator + readonly deletedGameMessage: Locator + + constructor(page: Page) { + this.page = page + + // 네비게이션 영역 + this.reportsTab = page.getByRole("link", { name: "신고접수" }) + this.gamesTab = page.getByRole("link", { name: "게임관리" }) + this.usersTab = page.getByRole("link", { name: "회원관리" }) + + // 메인 콘텐츠 영역 + this.reportsTable = page.getByRole("table", { name: "신고접수 테이블" }) + this.reportedGameRows = this.reportsTable.locator('[tabindex="0"]') + + // 팝업 + this.reportDetail = page.getByRole("dialog") + this.reportDetailCloseButton = page.getByRole("button", { + name: "게임 미리보기 닫기", + }) + this.reportIgnoreButton = page.getByRole("button", { name: "신고 무시" }) + this.gameDeleteButton = page.getByRole("button", { name: "게임 삭제" }) + this.gameQuestionList = page.getByTestId("game-preview-questions") + this.gameQuestionCount = page.getByTestId("game-preview-question-count") + this.reportedGameTitle = page.getByTestId("game-preview-game-title") + this.gameReportTable = page.getByRole("table", { + name: "게임 신고 정보 테이블", + }) + this.blockCreatorButton = page.getByRole("button", { + name: /^제작자.*차단$/, + }) + this.blockReporterButton = page.getByRole("button", { + name: /^신고자.*차단$/, + }) + this.gameCreatorInfo = page.getByRole("cell", { + name: "제작자 이름과 이메일", + }) + this.gameReporterInfo = page.getByRole("cell", { + name: "신고자 이름과 이메일", + }) + this.reportReasonInfo = page.getByRole("cell", { name: "신고 사유" }) + this.deletedGameMessage = page.getByText("삭제 처리된 게임입니다.") + } + + async navigateToReports() { + await this.page.goto("/admin/reports") + } + + async checkNavigationHighlight() { + await expect(this.reportsTab).toHaveAttribute("aria-current", "page") + await expect(this.gamesTab).not.toHaveAttribute("aria-current", "page") + await expect(this.usersTab).not.toHaveAttribute("aria-current", "page") + } + + async checkReportedTable() { + await expect(this.reportsTable).toBeVisible() + } + + async clickGamesTab() { + await this.gamesTab.click() + } + + async clickUsersTab() { + await this.usersTab.click() + } + + async checkRowCount() { + const count = await this.reportedGameRows.count() + expect(count).toBeLessThanOrEqual(7) + } + + async checkRowColumns() { + const headerRow = this.reportsTable.getByRole("row", { + name: "신고접수 테이블 헤더", + }) + const headers = headerRow.getByRole("columnheader") + await expect(headers).toHaveCount(6) + + const headerTexts = await headers.allTextContents() + expect(headerTexts).toContain("번호") + expect(headerTexts).toContain("게임명") + expect(headerTexts).toContain("제작자") + expect(headerTexts).toContain("신고자") + expect(headerTexts).toContain("신고일자") + expect(headerTexts).toContain("처리 여부") + } + + async clickReportedGameRow(index: number) { + await this.reportedGameRows.nth(index).click() + } + + async checkReportDetailPopup() { + await expect(this.reportDetail).toBeVisible() + } + + async clickCloseButton() { + await this.reportDetailCloseButton.click() + } + + async checkGameInfo() { + await expect(this.reportedGameTitle).toBeVisible() + await expect(this.gameQuestionCount).toBeVisible() + await expect(this.reportReasonInfo).toBeVisible() + } + + async checkCarousel() { + await expect(this.gameQuestionList).toBeVisible() + } + + async checkDeletedGameMessage() { + await expect(this.deletedGameMessage).toBeVisible() + } + + async clickIgnoreReport() { + await this.reportIgnoreButton.click() + } + + async clickDeleteGame() { + await this.gameDeleteButton.click() + } + + async clickBlockCreator() { + await this.blockCreatorButton.click() + } + + async clickBlockReporter() { + await this.blockReporterButton.click() + } + + async checkButtonDisabled(button: Locator) { + await expect(button).toBeDisabled() + } +} From c582a1651a7bb0b6c48d4b6bd401f6e28b5df41e Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:34:12 +0900 Subject: [PATCH 28/36] refactor(msw): change admin handler to be fixture based --- .../app/src/entities/report/model/types.ts | 7 +- service/app/src/mocks/data/admin.ts | 5 +- .../app/src/mocks/fixtures/adminReports.ts | 22 ++ service/app/src/mocks/handlers/admin.ts | 256 ++++++++++-------- service/app/src/mocks/handlers/index.ts | 17 +- 5 files changed, 188 insertions(+), 119 deletions(-) create mode 100644 service/app/src/mocks/fixtures/adminReports.ts diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index 5cc432c3..1053b0c5 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -20,13 +20,15 @@ export interface AdminReportsResponse { hasNext: boolean } +export type AdminReportStatus = "PENDING" | "GAME_DELETED" | "IGNORED" + export interface AdminReport { reportId: number gameName: string creatorName: string reporterName: string reportedAt: string - status: "PENDING" | "GAME_DELETED" | "IGNORED" + status: AdminReportStatus } export const adminReportUpdateStatusSchema = z.enum([ @@ -54,6 +56,7 @@ export const reportIdParamsSchema = z.object({ export type ReportIdParams = z.infer export interface ReportDetailData { + status: AdminReportStatus gameTitle: string makerNickname: string makerEmail: string @@ -70,4 +73,6 @@ export interface ReportDetailData { reporterEmail: string reporterNickname: string reasonCode: ReportReasonCode + isMakerBlock: boolean + isReporterBlock: boolean } diff --git a/service/app/src/mocks/data/admin.ts b/service/app/src/mocks/data/admin.ts index 783462be..45331d0f 100644 --- a/service/app/src/mocks/data/admin.ts +++ b/service/app/src/mocks/data/admin.ts @@ -39,8 +39,6 @@ const reporterNames = [ "신고자10", ] -const statuses: AdminReport["status"][] = ["PENDING", "GAME_DELETED", "IGNORED"] - export const generateMockAdminReports = ( count: number, seed?: number, @@ -61,7 +59,6 @@ export const generateMockAdminReports = ( const randomIndex = Math.floor(Math.random() * gameNames.length) const creatorIndex = Math.floor(Math.random() * creatorNames.length) const reporterIndex = Math.floor(Math.random() * reporterNames.length) - const statusIndex = Math.floor(Math.random() * statuses.length) const daysAgo = Math.floor(Math.random() * 30) const date = new Date() @@ -73,7 +70,7 @@ export const generateMockAdminReports = ( creatorName: creatorNames[creatorIndex], reporterName: reporterNames[reporterIndex], reportedAt: date.toISOString(), - status: statuses[statusIndex], + status: "PENDING", }) } diff --git a/service/app/src/mocks/fixtures/adminReports.ts b/service/app/src/mocks/fixtures/adminReports.ts new file mode 100644 index 00000000..5faa7d3c --- /dev/null +++ b/service/app/src/mocks/fixtures/adminReports.ts @@ -0,0 +1,22 @@ +import type { AdminReport } from "@/entities/report/model/types" + +export const adminReportFixtures = { + pending: (id: number): AdminReport => ({ + reportId: id, + gameName: "테스트 게임", + creatorName: "김관리", + reporterName: "박신고", + reportedAt: "2026-01-01T00:00:00.000Z", + status: "PENDING", + }), + + deleted: (id: number): AdminReport => ({ + ...adminReportFixtures.pending(id), + status: "GAME_DELETED", + }), + + ignored: (id: number): AdminReport => ({ + ...adminReportFixtures.pending(id), + status: "IGNORED", + }), +} diff --git a/service/app/src/mocks/handlers/admin.ts b/service/app/src/mocks/handlers/admin.ts index 2541f299..232a7cf6 100644 --- a/service/app/src/mocks/handlers/admin.ts +++ b/service/app/src/mocks/handlers/admin.ts @@ -1,13 +1,15 @@ import { http, HttpResponse } from "msw" -import { +import type { + AdminReport, AdminReportsResponse, +} from "@/entities/report/model/types" +import { adminReportUpdateRequestSchema, reportIdParamsSchema, } from "@/entities/report/model/types" import { blockUsersRequestSchema } from "@/entities/suspension/model/types" -import { mockAdminReports } from "../data/admin" import { internalServerError, loginRequiredError } from "../data/common" import { gameMissingFieldsError, @@ -21,131 +23,161 @@ import { generateSuccessResponse } from "../utils/responseHelpers" const MSW_BASE_URL = process.env.MSW_BASE_URL || "http://localhost:3000" const PAGE_SIZE = 7 -const TOTAL_ELEMENTS = 21 - -export const adminHandlers = [ - http.get(`${MSW_BASE_URL}/admin/games`, ({ request }) => { - try { - const cookieHeader = request.headers.get("Cookie") - if (!validateSessionCookie(cookieHeader)) { - return HttpResponse.json(loginRequiredError, { status: 401 }) + +export const createAdminHandlers = (initialReports: AdminReport[]) => { + const store: AdminReport[] = structuredClone(initialReports) + const blockedEmails = new Set() + + return [ + http.get(`${MSW_BASE_URL}/admin/games`, ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const url = new URL(request.url) + const pageParam = url.searchParams.get("page") + const page = pageParam ? Math.max(0, parseInt(pageParam, 10)) : 0 + + const totalElements = store.length + const totalPages = Math.ceil(totalElements / PAGE_SIZE) + + const startIndex = page * PAGE_SIZE + const endIndex = startIndex + PAGE_SIZE + + const paginatedReports = store.slice(startIndex, endIndex) + const hasNext = page < totalPages - 1 + + const response: AdminReportsResponse = { + report: paginatedReports, + page, + size: PAGE_SIZE, + totalElements, + totalPages, + hasNext, + } + + return HttpResponse.json(generateSuccessResponse(response)) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) + } + }), + + http.get(`${MSW_BASE_URL}/admin/games/:reportId`, ({ request, params }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } + + const parsed = reportIdParamsSchema.safeParse(params) + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } + + const { reportId } = parsed.data + const report = store.find((r) => r.reportId === reportId) + + if (!report) { + return HttpResponse.json(reportNotFoundError(reportId), { + status: 404, + }) + } + + const makerNickname = report.creatorName + const makerEmail = `${makerNickname + .replace(/\s+/g, ".") + .toLowerCase()}@example.com` + + const reporterNickname = report.reporterName + const reporterEmail = `${reporterNickname + .replace(/\s+/g, ".") + .toLowerCase()}@example.com` + + const questionCount = 10 + const questions = generateMockQuestions(questionCount, 1) + + const data = { + gameTitle: report.gameName, + status: report.status, + makerNickname, + makerEmail, + questionCount, + version: 1, + questions, + reporterEmail, + reporterNickname, + reasonCode: "VIOLENT_OR_DISTURBING_CONTENT", + isMakerBlock: blockedEmails.has(makerEmail), + isReporterBlock: blockedEmails.has(reporterEmail), + } + + return HttpResponse.json(generateSuccessResponse(data)) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) } + }), - const url = new URL(request.url) - const pageParam = url.searchParams.get("page") - const page = pageParam ? Math.max(0, parseInt(pageParam, 10)) : 0 + http.post(`${MSW_BASE_URL}/admin/games/delete`, async ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } - const totalPages = Math.ceil(TOTAL_ELEMENTS / PAGE_SIZE) - const startIndex = page * PAGE_SIZE - const endIndex = startIndex + PAGE_SIZE + const json = await request.json() + const parsed = adminReportUpdateRequestSchema.safeParse(json) - const paginatedReports = mockAdminReports.slice(startIndex, endIndex) - const hasNext = page < totalPages - 1 + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } - const response: AdminReportsResponse = { - report: paginatedReports, - page, - size: PAGE_SIZE, - totalElements: TOTAL_ELEMENTS, - totalPages, - hasNext, - } + const { reportId, status } = parsed.data - return HttpResponse.json(generateSuccessResponse(response)) - } catch { - return HttpResponse.json(internalServerError, { status: 500 }) - } - }), - http.get(`${MSW_BASE_URL}/admin/games/:reportId`, ({ request, params }) => { - try { - const cookieHeader = request.headers.get("Cookie") - if (!validateSessionCookie(cookieHeader)) { - return HttpResponse.json(loginRequiredError, { status: 401 }) - } + const idx = store.findIndex((r) => r.reportId === reportId) - const parsed = reportIdParamsSchema.safeParse(params) - if (!parsed.success) { - return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) - } + if (idx === -1) { + return HttpResponse.json(reportNotFoundError(reportId), { + status: 404, + }) + } - const { reportId } = parsed.data - const report = mockAdminReports.find((r) => r.reportId === reportId) - if (!report) { - return HttpResponse.json(reportNotFoundError(reportId), { - status: 404, - }) - } + const nextStatus = status === "DELETE_GAME" ? "GAME_DELETED" : "IGNORED" - const makerNickname = report.creatorName - const makerEmail = `${makerNickname.replace(/\s+/g, ".").toLowerCase()}@example.com` - const reporterNickname = report.reporterName - const reporterEmail = `${reporterNickname.replace(/\s+/g, ".").toLowerCase()}@example.com` - - const questionCount = 10 - const questions = generateMockQuestions(questionCount, 1) - - const data = { - gameTitle: report.gameName, - makerNickname, - makerEmail, - questionCount, - version: 1, - questions, - reporterEmail, - reporterNickname, - reasonCode: "VIOLENT_OR_DISTURBING_CONTENT", - } + store[idx] = { + ...store[idx], + status: nextStatus, + } - return HttpResponse.json(generateSuccessResponse(data)) - } catch { - return HttpResponse.json(internalServerError, { status: 500 }) - } - }), - http.post(`${MSW_BASE_URL}/admin/games/delete`, async ({ request }) => { - try { - const cookieHeader = request.headers.get("Cookie") - if (!validateSessionCookie(cookieHeader)) { - return HttpResponse.json(loginRequiredError, { status: 401 }) + return HttpResponse.json(gameSuccessResponse()) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) } + }), - const json = await request.json() - const parsed = adminReportUpdateRequestSchema.safeParse(json) + http.post(`${MSW_BASE_URL}/admin/users/block`, async ({ request }) => { + try { + const cookieHeader = request.headers.get("Cookie") + if (!validateSessionCookie(cookieHeader)) { + return HttpResponse.json(loginRequiredError, { status: 401 }) + } - if (!parsed.success) { - return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) - } + const json = await request.json() + const parsed = blockUsersRequestSchema.safeParse(json) - const { reportId } = parsed.data + if (!parsed.success) { + return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) + } - const report = mockAdminReports.find((r) => r.reportId === reportId) - if (!report) { - return HttpResponse.json(reportNotFoundError(reportId), { - status: 404, + parsed.data.banList.forEach(({ email }) => { + blockedEmails.add(email) }) - } - return HttpResponse.json(gameSuccessResponse()) - } catch { - return HttpResponse.json(internalServerError, { status: 500 }) - } - }), - http.post(`${MSW_BASE_URL}/admin/users/block`, async ({ request }) => { - try { - const cookieHeader = request.headers.get("Cookie") - if (!validateSessionCookie(cookieHeader)) { - return HttpResponse.json(loginRequiredError, { status: 401 }) + return HttpResponse.json(gameSuccessResponse()) + } catch { + return HttpResponse.json(internalServerError, { status: 500 }) } - - const json = await request.json() - const parsed = blockUsersRequestSchema.safeParse(json) - - if (!parsed.success) { - return HttpResponse.json(gameMissingFieldsError(), { status: 400 }) - } - - return HttpResponse.json(gameSuccessResponse()) - } catch { - return HttpResponse.json(internalServerError, { status: 500 }) - } - }), -] + }), + ] +} diff --git a/service/app/src/mocks/handlers/index.ts b/service/app/src/mocks/handlers/index.ts index 5b0488a6..cb503377 100644 --- a/service/app/src/mocks/handlers/index.ts +++ b/service/app/src/mocks/handlers/index.ts @@ -1,5 +1,18 @@ -import { adminHandlers } from "./admin" +import { adminReportFixtures } from "../fixtures/adminReports" +import { createAdminHandlers } from "./admin" import { authHandlers } from "./auth" import { gameHandlers } from "./game" -export const handlers = [...authHandlers, ...gameHandlers, ...adminHandlers] +export const handlers = [ + ...authHandlers, + ...gameHandlers, + ...createAdminHandlers([ + adminReportFixtures.pending(1), + adminReportFixtures.deleted(2), + adminReportFixtures.pending(3), + adminReportFixtures.pending(4), + adminReportFixtures.pending(5), + adminReportFixtures.pending(6), + adminReportFixtures.pending(7), + ]), +] From f60de70fdd5fdcc948c1ae65c85536309c0186d2 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:35:34 +0900 Subject: [PATCH 29/36] feat(reportdetail): add block state and report status --- .../game/ui/GamePreview/gamePreviewAdmin.tsx | 27 ++++++++++-- .../GamePreview/gamePreviewQuestionList.tsx | 42 +++++++------------ .../game/ui/GamePreview/gamePreviewUser.tsx | 13 ++++-- 3 files changed, 48 insertions(+), 34 deletions(-) diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx index 134308e5..d6710368 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewAdmin.tsx @@ -2,6 +2,8 @@ import { PrimaryBoxButton } from "@ject-5-fe/design/components/button" +import type { AdminReportStatus } from "@/entities/report/model/types" + import { GamePreviewQuestionList } from "./gamePreviewQuestionList" import { GamePreviewReportTable } from "./gamePreviewReportTable" import { GamePreviewShell } from "./gamePreviewShell" @@ -20,13 +22,16 @@ interface GamePreviewAdminProps { creator: { name: string email: string + isBlocked: boolean } reporter: { name: string email: string + isBlocked: boolean } category: string } + status: AdminReportStatus onClose?: () => void onIgnoreReport?: () => void onDeleteGame?: () => void @@ -38,6 +43,7 @@ interface GamePreviewAdminProps { export const GamePreviewAdmin = ({ className = "", gameTitle, + status, questionCount = 10, questions = [], reportData, @@ -59,6 +65,7 @@ export const GamePreviewAdmin = ({ size="lg" _style="solid" onClick={onIgnoreReport} + disabled={status === "GAME_DELETED"} > 신고 무시 @@ -67,16 +74,28 @@ export const GamePreviewAdmin = ({ _style="solid" onClick={onDeleteGame} className="bg-background-interactive-destructive" + disabled={status === "GAME_DELETED"} > 게임 삭제
- +
+ + 총 {questionCount} 문제 + + {status === "GAME_DELETED" ? ( +
+ 삭제 처리된 게임입니다. +
+ ) : ( + + )} +
{reportData && ( { return ( -
- - 총 {questionCount} 문제 - -
- {questions.map((question) => ( - - - {question.title} - - ))} -
+
+ {questions.map((question) => ( + + + {question.title} + + ))}
) } diff --git a/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx index 50988659..22bc74ae 100644 --- a/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx +++ b/service/app/src/entities/game/ui/GamePreview/gamePreviewUser.tsx @@ -52,10 +52,15 @@ export const GamePreviewUser = ({
- +
+ + 총 {questionCount} 문제 + + +
From 89ea011fb5c617aa3451dc115454a5d87f9fb8d9 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:36:35 +0900 Subject: [PATCH 30/36] feat(reports): use react query on reportDetail api --- .../game/hooks/useGameReportDetail.tsx | 114 +++++++++++------- .../report/hooks/useAdminReportMutations.ts | 2 +- .../entities/report/hooks/useReportDetail.ts | 27 +++++ 3 files changed, 99 insertions(+), 44 deletions(-) create mode 100644 service/app/src/entities/report/hooks/useReportDetail.ts diff --git a/service/app/src/entities/game/hooks/useGameReportDetail.tsx b/service/app/src/entities/game/hooks/useGameReportDetail.tsx index 3e4c1618..4cc7a2fa 100644 --- a/service/app/src/entities/game/hooks/useGameReportDetail.tsx +++ b/service/app/src/entities/game/hooks/useGameReportDetail.tsx @@ -5,6 +5,8 @@ import { REPORT_REASON_LABELS } from "@/entities/game/model/report" import { GamePreviewAdmin } from "@/entities/game/ui/GamePreview" import { getReportDetail } from "@/entities/report/api/getReportDetail" import { useAdminReportMutations } from "@/entities/report/hooks/useAdminReportMutations" +import { useReportDetail } from "@/entities/report/hooks/useReportDetail" +import { queryClient } from "@/shared/lib/queryClient" export const useAdminGamePreview = () => { const { ignoreReportMutation, deleteGameMutation, blockUserMutation } = @@ -12,56 +14,82 @@ export const useAdminGamePreview = () => { const openAdminPreview = useCallback( async (reportId: number) => { - const data = await getReportDetail(reportId) + await queryClient.fetchQuery({ + queryKey: ["reportDetail", reportId] as const, + queryFn: async () => { + const response = await getReportDetail(reportId) + return response + }, + }) overlay.open(({ close, isOpen }) => { - const handleIgnoreReport = async () => { - await ignoreReportMutation.mutateAsync(reportId) - close() - } + const Content = () => { + const { data } = useReportDetail({ + reportId, + enabled: isOpen, + }) - const handleDeleteGame = async () => { - await deleteGameMutation.mutateAsync(reportId) - close() - } + const handleIgnoreReport = async () => { + await ignoreReportMutation.mutateAsync(reportId) + close() + } - const handleBlockCreator = async () => { - await blockUserMutation.mutateAsync(data.makerEmail) - } + const handleDeleteGame = async () => { + await deleteGameMutation.mutateAsync(reportId) + close() + } + + const handleBlockCreator = async () => { + if (data) { + await blockUserMutation.mutateAsync(data.makerEmail) + } + } + + const handleBlockReporter = async () => { + if (data) { + await blockUserMutation.mutateAsync(data.reporterEmail) + } + } + + if (!data) { + return null + } - const handleBlockReporter = async () => { - await blockUserMutation.mutateAsync(data.reporterEmail) + return ( + ({ + id: q.questionId, + title: q.questionText, + imageUrl: q.imageUrl, + }))} + reportData={{ + creator: { + name: data.makerNickname, + email: data.makerEmail, + isBlocked: data.isMakerBlock, + }, + reporter: { + name: data.reporterNickname, + email: data.reporterEmail, + isBlocked: data.isReporterBlock, + }, + category: + REPORT_REASON_LABELS[data.reasonCode] ?? data.reasonCode, + }} + status={data.status} + onClose={close} + isOpen={isOpen} + onIgnoreReport={handleIgnoreReport} + onDeleteGame={handleDeleteGame} + onBlockCreator={handleBlockCreator} + onBlockReporter={handleBlockReporter} + /> + ) } - return ( - ({ - id: q.questionId, - title: q.questionText, - imageUrl: q.imageUrl, - }))} - reportData={{ - creator: { - name: data.makerNickname, - email: data.makerEmail, - }, - reporter: { - name: data.reporterNickname, - email: data.reporterEmail, - }, - category: - REPORT_REASON_LABELS[data.reasonCode] ?? data.reasonCode, - }} - onClose={close} - isOpen={isOpen} - onIgnoreReport={handleIgnoreReport} - onDeleteGame={handleDeleteGame} - onBlockCreator={handleBlockCreator} - onBlockReporter={handleBlockReporter} - /> - ) + return }) }, [ignoreReportMutation, deleteGameMutation, blockUserMutation], diff --git a/service/app/src/entities/report/hooks/useAdminReportMutations.ts b/service/app/src/entities/report/hooks/useAdminReportMutations.ts index fa7d2958..8ae78aa9 100644 --- a/service/app/src/entities/report/hooks/useAdminReportMutations.ts +++ b/service/app/src/entities/report/hooks/useAdminReportMutations.ts @@ -28,7 +28,7 @@ export const useAdminReportMutations = () => { banList: [{ email }], }), onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["adminReports"] }) + queryClient.invalidateQueries({ queryKey: ["reportDetail"] }) }, }) diff --git a/service/app/src/entities/report/hooks/useReportDetail.ts b/service/app/src/entities/report/hooks/useReportDetail.ts new file mode 100644 index 00000000..1b55773b --- /dev/null +++ b/service/app/src/entities/report/hooks/useReportDetail.ts @@ -0,0 +1,27 @@ +import { useQuery } from "@tanstack/react-query" + +import { getReportDetail } from "../api/getReportDetail" +import type { ReportDetailData } from "../model/types" + +interface UseReportDetailParams { + reportId: number + enabled?: boolean +} + +export const useReportDetail = ({ + reportId, + enabled = true, +}: UseReportDetailParams) => { + const { data } = useQuery({ + queryKey: ["reportDetail", reportId] as const, + queryFn: async () => { + const response = await getReportDetail(reportId) + return response + }, + enabled, + staleTime: 0, + retry: 2, + }) + + return { data } +} From 61dc5ddf8396afce5eed1a4d13a8b6d1050c7912 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 11:45:05 +0900 Subject: [PATCH 31/36] fix: remove unused export --- service/app/src/shared/ui/admin-table/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/service/app/src/shared/ui/admin-table/index.ts b/service/app/src/shared/ui/admin-table/index.ts index eb50b8a0..51a6daab 100644 --- a/service/app/src/shared/ui/admin-table/index.ts +++ b/service/app/src/shared/ui/admin-table/index.ts @@ -2,4 +2,3 @@ export * from "./AdminTableGroup" export * from "./AdminTableRoot" export * from "./AdminTableRow" export * from "./AdminTableText" -export * from "./useRowSelection" From 403992895daa56a6c0294eafa30a43a6ab9987be Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 12:13:34 +0900 Subject: [PATCH 32/36] test(reports): update test logic --- .../src/app/admin/reports/__tests__/reports.spec.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/service/app/src/app/admin/reports/__tests__/reports.spec.ts b/service/app/src/app/admin/reports/__tests__/reports.spec.ts index 5a9df4af..3d085933 100644 --- a/service/app/src/app/admin/reports/__tests__/reports.spec.ts +++ b/service/app/src/app/admin/reports/__tests__/reports.spec.ts @@ -125,10 +125,10 @@ test.describe("신고접수 페이지", () => { await pom.clickDeleteGame() await expect(pom.reportDetail).not.toBeVisible() - const updatedAriaLabel = await pom.reportedGameRows - .nth(1) - .getAttribute("aria-label") - expect(updatedAriaLabel).toContain("처리 여부 삭제 완료") + await expect(pom.reportedGameRows.nth(1)).toHaveAttribute( + "aria-label", + /처리 여부 삭제 완료/, + ) }) }) @@ -173,8 +173,7 @@ test.describe("신고접수 페이지", () => { await pom.clickIgnoreReport() await expect(pom.reportDetail).not.toBeVisible() - const updatedAriaLabel = await row.getAttribute("aria-label") - expect(updatedAriaLabel).toContain("처리 여부 신고 무시") + await expect(row).toHaveAttribute("aria-label", /처리 여부 신고 무시/) }) }) From 00f2b8fcc67d0234902151fd56b37ad6c3075a51 Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 14:43:43 +0900 Subject: [PATCH 33/36] fix(session): update type --- service/app/src/entities/auth/api/validateSession.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/service/app/src/entities/auth/api/validateSession.ts b/service/app/src/entities/auth/api/validateSession.ts index ad2bff44..dbb57e8d 100644 --- a/service/app/src/entities/auth/api/validateSession.ts +++ b/service/app/src/entities/auth/api/validateSession.ts @@ -1,9 +1,9 @@ import { fetchClient } from "@/shared/api/fetchClient" -import { UserSessionResponse } from "../model/auth" +import type { UserSession, UserSessionResponse } from "../model/auth" export const validateSession = async (): Promise => { - const response = await fetchClient.get("users/auth/me") + const response = await fetchClient.get("users/auth/me") return response.json() } From 4e9b7c1fa139f92edc78a32e5580a75b24795cae Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 14:44:17 +0900 Subject: [PATCH 34/36] fix: fix typo --- service/app/src/entities/game/hooks/useGameReportDetail.tsx | 2 +- service/app/src/entities/report/model/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/service/app/src/entities/game/hooks/useGameReportDetail.tsx b/service/app/src/entities/game/hooks/useGameReportDetail.tsx index 4cc7a2fa..8cd47d90 100644 --- a/service/app/src/entities/game/hooks/useGameReportDetail.tsx +++ b/service/app/src/entities/game/hooks/useGameReportDetail.tsx @@ -58,7 +58,7 @@ export const useAdminGamePreview = () => { return ( ({ id: q.questionId, title: q.questionText, diff --git a/service/app/src/entities/report/model/types.ts b/service/app/src/entities/report/model/types.ts index 1053b0c5..aa1095b1 100644 --- a/service/app/src/entities/report/model/types.ts +++ b/service/app/src/entities/report/model/types.ts @@ -60,7 +60,7 @@ export interface ReportDetailData { gameTitle: string makerNickname: string makerEmail: string - quetionCount: number + questionCount: number version: number questions: { questionId: number From e5ea0a6bf35bee1845e170a75c0c5b49fbb9fbdc Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 14:44:48 +0900 Subject: [PATCH 35/36] refactor(report): use async / await --- service/app/src/entities/report/api/getReportDetail.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/service/app/src/entities/report/api/getReportDetail.ts b/service/app/src/entities/report/api/getReportDetail.ts index a79899b8..5cafe850 100644 --- a/service/app/src/entities/report/api/getReportDetail.ts +++ b/service/app/src/entities/report/api/getReportDetail.ts @@ -6,5 +6,6 @@ export const getReportDetail = async (reportId: number) => { const response = await fetchClient.get( `admin/games/${reportId}`, ) - return response.json().then((res) => res.data) + const res = await response.json() + return res.data } From 318b5938b353ad473a8865d0a45f147776b72dee Mon Sep 17 00:00:00 2001 From: kimnamheeee Date: Fri, 23 Jan 2026 14:45:14 +0900 Subject: [PATCH 36/36] test(reports): update test selector --- .../app/src/app/admin/reports/__tests__/reports.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/service/app/src/app/admin/reports/__tests__/reports.spec.ts b/service/app/src/app/admin/reports/__tests__/reports.spec.ts index 3d085933..185b104e 100644 --- a/service/app/src/app/admin/reports/__tests__/reports.spec.ts +++ b/service/app/src/app/admin/reports/__tests__/reports.spec.ts @@ -122,13 +122,12 @@ test.describe("신고접수 페이지", () => { test("삭제되지 않은 게임에서 게임 삭제 클릭시 팝업이 닫히고 상태가 삭제 완료로 업데이트된다", async () => { await pom.clickReportedGameRow(0) + const row = pom.reportedGameRows.first() + await pom.clickDeleteGame() await expect(pom.reportDetail).not.toBeVisible() - await expect(pom.reportedGameRows.nth(1)).toHaveAttribute( - "aria-label", - /처리 여부 삭제 완료/, - ) + await expect(row).toHaveAttribute("aria-label", /처리 여부 삭제 완료/) }) })