From a2e4926273356a0845b9fc394b174b4038b04935 Mon Sep 17 00:00:00 2001 From: mendel Date: Thu, 13 Jul 2023 16:53:48 +0900 Subject: [PATCH 001/180] =?UTF-8?q?feat:=20=EC=95=88=EB=93=9C=EB=A1=9C?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/.gitignore | 33 ++++ android/app/.gitignore | 1 + android/app/build.gradle | 44 +++++ android/app/proguard-rules.pro | 21 ++ .../android/ExampleInstrumentedTest.kt | 22 +++ android/app/src/main/AndroidManifest.xml | 26 +++ .../ddangddangddang/android/MainActivity.kt | 11 ++ .../drawable-v24/ic_launcher_foreground.xml | 30 +++ .../res/drawable/ic_launcher_background.xml | 170 ++++++++++++++++ .../app/src/main/res/layout/activity_main.xml | 18 ++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 6 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 6 + .../src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes .../res/mipmap-hdpi/ic_launcher_round.webp | Bin 0 -> 2898 bytes .../src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes .../res/mipmap-mdpi/ic_launcher_round.webp | Bin 0 -> 1772 bytes .../main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../res/mipmap-xhdpi/ic_launcher_round.webp | Bin 0 -> 3918 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../res/mipmap-xxhdpi/ic_launcher_round.webp | Bin 0 -> 5914 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.webp | Bin 0 -> 7778 bytes .../app/src/main/res/values-night/themes.xml | 7 + android/app/src/main/res/values/colors.xml | 5 + android/app/src/main/res/values/strings.xml | 3 + android/app/src/main/res/values/themes.xml | 9 + android/app/src/main/res/xml/backup_rules.xml | 13 ++ .../main/res/xml/data_extraction_rules.xml | 19 ++ .../android/ExampleUnitTest.kt | 16 ++ android/build.gradle | 6 + android/gradle.properties | 23 +++ android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + android/gradlew | 185 ++++++++++++++++++ android/gradlew.bat | 89 +++++++++ android/settings.gradle | 16 ++ 36 files changed, 785 insertions(+) create mode 100644 android/.gitignore create mode 100644 android/app/.gitignore create mode 100644 android/app/build.gradle create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/androidTest/java/com/ddangddangddang/android/ExampleInstrumentedTest.kt create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/com/ddangddangddang/android/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/app/src/main/res/layout/activity_main.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp create mode 100644 android/app/src/main/res/values-night/themes.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/themes.xml create mode 100644 android/app/src/main/res/xml/backup_rules.xml create mode 100644 android/app/src/main/res/xml/data_extraction_rules.xml create mode 100644 android/app/src/test/java/com/ddangddangddang/android/ExampleUnitTest.kt create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 000000000..347e252ef --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,33 @@ +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Log/OS Files +*.log + +# Android Studio generated files and folders +captures/ +.externalNativeBuild/ +.cxx/ +*.apk +output.json + +# IntelliJ +*.iml +.idea/ +misc.xml +deploymentTargetDropDown.xml +render.experimental.xml + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Android Profiling +*.hprof diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 000000000..99248b56d --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,44 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.ddangddangddang.android' + compileSdk 33 + + defaultConfig { + applicationId "com.ddangddangddang.android" + minSdk 28 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation 'androidx.core:core-ktx:1.8.0' + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/ddangddangddang/android/ExampleInstrumentedTest.kt b/android/app/src/androidTest/java/com/ddangddangddang/android/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..bec464668 --- /dev/null +++ b/android/app/src/androidTest/java/com/ddangddangddang/android/ExampleInstrumentedTest.kt @@ -0,0 +1,22 @@ +package com.ddangddangddang.android + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ddangddangddang.android", appContext.packageName) + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..18de0e8fc --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/com/ddangddangddang/android/MainActivity.kt b/android/app/src/main/java/com/ddangddangddang/android/MainActivity.kt new file mode 100644 index 000000000..4810fb97b --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/MainActivity.kt @@ -0,0 +1,11 @@ +package com.ddangddangddang.android + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 000000000..2b068d114 --- /dev/null +++ b/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 000000000..07d5da9cb --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..17eab17ba --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..6f3b755bf --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..b2dfe3d1ba5cf3ee31b3ecc1ced89044a1f3b7a9 GIT binary patch literal 2898 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!TQj4N+cqN`nQhxvX7dAV-`K|Ub$-q+H-5I?Tx0g9jWxd@A|?POE8`3b8fO$T))xP* z(X?&brZw({`)WU&rdAs1iTa0x6F@PIxJ&&L|dpySV!ID|iUhjCcKz(@mE z!x@~W#3H<)4Ae(4eQJRk`Iz3<1)6^m)0b_4_TRZ+cz#eD3f8V;2r-1fE!F}W zEi0MEkTTx}8i1{`l_6vo0(Vuh0HD$I4SjZ=?^?k82R51bC)2D_{y8mi_?X^=U?2|F{Vr7s!k(AZC$O#ZMyavHhlQ7 zUR~QXuH~#o#>(b$u4?s~HLF*3IcF7023AlwAYudn0FV~|odGH^05AYPEfR)8p`i{n zwg3zPVp{+wOsxKc>)(pMupKF!Y2HoUqQ3|Yu|8lwR=?5zZuhG6J?H`bSNk_wPoM{u zSL{c@pY7+c2kck>`^q1^^gR0QB7Y?KUD{vz-uVX~;V-rW)PDcI)$_UjgVV?S?=oLR zf4}zz{#*R_{LkiJ#0RdQLNC^2Vp%JPEUvG9ra2BVZ92(p9h7Ka@!yf9(lj#}>+|u* z;^_?KWdzkM`6gqPo9;;r6&JEa)}R3X{(CWv?NvgLeOTq$cZXqf7|sPImi-7cS8DCN zGf;DVt3Am`>hH3{4-WzH43Ftx)SofNe^-#|0HdCo<+8Qs!}TZP{HH8~z5n`ExcHuT zDL1m&|DVpIy=xsLO>8k92HcmfSKhflQ0H~9=^-{#!I1g(;+44xw~=* zxvNz35vfsQE)@)Zsp*6_GjYD};Squ83<_?^SbALb{a`j<0Gn%6JY!zhp=Fg}Ga2|8 z52e1WU%^L1}15Ex0fF$e@eCT(()_P zvV?CA%#Sy08_U6VPt4EtmVQraWJX` zh=N|WQ>LgrvF~R&qOfB$!%D3cGv?;Xh_z$z7k&s4N)$WYf*k=|*jCEkO19{h_(%W4 zPuOqbCw`SeAX*R}UUsbVsgtuG?xs(#Ikx9`JZoQFz0n*7ZG@Fv@kZk`gzO$HoA9kN z8U5{-yY zvV{`&WKU2$mZeoBmiJrEdzUZAv1sRxpePdg1)F*X^Y)zp^Y*R;;z~vOv-z&)&G)JQ{m!C9cmziu1^nHA z`#`0c>@PnQ9CJKgC5NjJD8HM3|KC(g5nnCq$n0Gsu_DXk36@ql%npEye|?%RmG)

FJ$wK}0tWNB{uH;AM~i literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..948a3070fe34c611c42c0d3ad3013a0dce358be0 GIT binary patch literal 1900 zcmV-y2b1_xNk&Fw2LJ$9MM6+kP&il$0000G0001A003VA06|PpNH75a00DqwTbm-~ zullQTcXxO9ki!OCRx^i?oR|n!<8G0=kI^!JSjFi-LL*`V;ET0H2IXfU0*i>o6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000000000000000000000000000000000000..1b9a6956b3acdc11f40ce2bb3f6efbd845cc243f GIT binary patch literal 3918 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�(p-{x$&=IACIfoAR!za)F-^da-t3#0Dycnp zwO~NVXwXCl;jE<}>%@xz|=8fIJAB?>+E{7)|4l${4ngA3G|=r z2Dyv;VVWSgZx9Wj>qUjleGl3Ei9K4>h!(lPS%8VOG>Xu0%6VDz^O=bjJmuP7>DeUv zrbI}MlHB^^d?{zv6d=@_ZD2lg1&G7UjnVN{1}9WkaM3H~btX0GtSzB+tZ^qRgWo4m z!GmimlG$=wgXCnr6j@m<1gAL46#T~5Bnm=2{^@>|t&`9mkEPddj zAvG~@Tv~TAm2i%VW}R-g(Z0)z-Y|szHr@rk>4MAyG*Ma*7Yh#H7(!-5>DZ@8r;_dx z{prSe<>~099F8vsYd2xff7uAS%7{S)f(|@me3t2$iy&NEc7OUEchp@9A|X;;IA>8!oX+y(BKJ$EzV* znR$z;!L$s7uy@{OT~nG#B!NRraT8(X##Ho!0r_o@gg0CA-9H^;-uE&?$2$nHv_00o z%cbuUc-tCx$Uh&EZ4Nf4Zgqv)Y6>usG3>GeQnxx_Z6+PcbX-+ysbt1hQ`K1LDpOE? zrAhIZhSN9yVIAOa22gn577tbc&i3|3V8NWy&!tw##`}9*x}gtI^h1DzZRA>UuaJG) zaZ7j)dq!O}{?#8Y7~7i6fHh4{`pL?>-18|p!S75Y#^DM>-S3)vuZG+Q7l@ek zQP~#cBpWgg#mApc_sPYjpw8odQuRokmTkzcNl`^CcKB7e&;zViV;{Y{o^Y$%7i0m# z62%#1Lq!RC?}lK>%mp}T!3Xv;L*0v*>USLm``N%>w>@fwC+#T&Tx2bN4w(20JB}oU zuSa6v^kXi0xPs?pbaOHnyiqq6By1EZY9OZ^^QA>{q-Hsd&m`pbQ%8121aWG-F5xf zlZ%;B{;C>X19|`^_?dVyCq>n+41w7|!tUS!{9rHlbhX=SZO5CQ^;!Du_E7*`GiR^Q w)2!4MKjfSAeNo!9>IaV6aUZ*?W>} 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

UmY2RIYtNPVDpE$%vda+HD#3m z&VuXJ{BK&Qe+rBa7eq}Q(bq|tn(RrJAk|ztj2(i{d>nmQnM?;HF2k&9sA6up5tmjl z7lySlzMbifH17-m-Lwa_F&e7nOH?ESi3#ckR3tsM+jsck3`oG!uMS}|eAwVXv>}qxwq?QY%QJ0}r@^;fhuUA9W z*BVl>TGo&N004@xSiwDUXUvp51sVmqO3m)=B55aPwf@0=e}cN+$-BdKxY`YrT_4)0 z_d10#i44Q*rFr8MC>*)v$EJvz``(pb{e&*6k+b zsMz%($|1+8hn8c2?P(l@;Rb&CsZeYoCI3?2!LqjbwPXW3z4G$Qfj=cT5Yb%vY0(AX oeb?AaKtwrnc|$|zzw9vfvn^aJJ!zd)XFXqqy0000001=f@-~a#s literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/themes.xml b/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000..75ab1bc58 --- /dev/null +++ b/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 000000000..c8524cd96 --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000..c7e1d6ba6 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + DdangDdangDdang + \ No newline at end of file diff --git a/android/app/src/main/res/values/themes.xml b/android/app/src/main/res/values/themes.xml new file mode 100644 index 000000000..1d6580a8d --- /dev/null +++ b/android/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file From e24054916f2a79b4b1dfa49089e1fe56cd3be375 Mon Sep 17 00:00:00 2001 From: mendel Date: Sat, 15 Jul 2023 17:20:37 +0900 Subject: [PATCH 014/180] =?UTF-8?q?feat:=20=ED=94=84=EB=9E=98=EA=B7=B8?= =?UTF-8?q?=EB=A8=BC=ED=8A=B8=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../android/feature/home/HomeFragment.kt | 7 +++ .../android/feature/main/FragmentType.kt | 8 ++++ .../android/feature/main/MainActivity.kt | 47 ++++++++++++++++-- .../feature/main/MainBindingAdapter.kt | 12 +++++ .../android/feature/main/MainViewModel.kt | 35 +++++++++++++- .../feature/message/MessageFragment.kt | 7 +++ .../android/feature/mypage/MyPageFragment.kt | 7 +++ .../android/feature/search/SearchFragment.kt | 7 +++ .../app/src/main/res/layout/activity_main.xml | 48 ++++++++++++++----- .../app/src/main/res/layout/fragment_home.xml | 17 +++++++ .../src/main/res/layout/fragment_message.xml | 17 +++++++ .../src/main/res/layout/fragment_my_page.xml | 17 +++++++ .../src/main/res/layout/fragment_search.xml | 17 +++++++ .../main/res/menu/menu_bottom_navigation.xml | 4 +- android/app/src/main/res/values/strings.xml | 2 +- 15 files changed, 232 insertions(+), 20 deletions(-) create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/home/HomeFragment.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/main/FragmentType.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/main/MainBindingAdapter.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/message/MessageFragment.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/mypage/MyPageFragment.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/search/SearchFragment.kt create mode 100644 android/app/src/main/res/layout/fragment_home.xml create mode 100644 android/app/src/main/res/layout/fragment_message.xml create mode 100644 android/app/src/main/res/layout/fragment_my_page.xml create mode 100644 android/app/src/main/res/layout/fragment_search.xml diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/home/HomeFragment.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/home/HomeFragment.kt new file mode 100644 index 000000000..896ce0829 --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/home/HomeFragment.kt @@ -0,0 +1,7 @@ +package com.ddangddangddang.android.feature.home + +import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.FragmentHomeBinding +import com.ddangddangddang.android.util.binding.BindingFragment + +class HomeFragment : BindingFragment(R.layout.fragment_home) diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/main/FragmentType.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/main/FragmentType.kt new file mode 100644 index 000000000..122e1e31e --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/main/FragmentType.kt @@ -0,0 +1,8 @@ +package com.ddangddangddang.android.feature.main + +enum class FragmentType(val tag: String) { + HOME("fragment_home_tag"), + SEARCH("fragment_search_tag"), + MESSAGE("fragment_message_tag"), + MY_PAGE("fragment_my_page_tag"), +} diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainActivity.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainActivity.kt index 7861d758d..db08b92f9 100644 --- a/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainActivity.kt +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainActivity.kt @@ -1,12 +1,53 @@ package com.ddangddangddang.android.feature.main import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.viewModels +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.ActivityMainBinding +import com.ddangddangddang.android.feature.common.viewModelFactory +import com.ddangddangddang.android.feature.home.HomeFragment +import com.ddangddangddang.android.feature.message.MessageFragment +import com.ddangddangddang.android.feature.mypage.MyPageFragment +import com.ddangddangddang.android.feature.search.SearchFragment +import com.ddangddangddang.android.util.binding.BindingActivity -class MainActivity : AppCompatActivity() { +class MainActivity : BindingActivity(R.layout.activity_main) { + private val viewModel by viewModels { viewModelFactory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding.viewModel = viewModel + + setupViewModel() + } + + fun setupViewModel() { + viewModel.currentFragmentType.observe(this) { + changeFragment(it) + } + } + + private fun changeFragment(type: FragmentType) { + supportFragmentManager.commit { + setReorderingAllowed(true) + + supportFragmentManager.fragments.forEach(::hide) + + supportFragmentManager.findFragmentByTag(type.tag)?.let { + show(it) + } ?: createFragment(type).run { + add(R.id.fcv_container, this, type.tag) + } + } + } + + private fun createFragment(type: FragmentType): Fragment { + return when (type) { + FragmentType.HOME -> HomeFragment() + FragmentType.SEARCH -> SearchFragment() + FragmentType.MESSAGE -> MessageFragment() + FragmentType.MY_PAGE -> MyPageFragment() + } } } diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainBindingAdapter.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainBindingAdapter.kt new file mode 100644 index 000000000..f1a108d98 --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainBindingAdapter.kt @@ -0,0 +1,12 @@ +package com.ddangddangddang.android.feature.main + +import androidx.databinding.BindingAdapter +import com.google.android.material.bottomnavigation.BottomNavigationView +import com.google.android.material.navigation.NavigationBarView + +@BindingAdapter("onNavigationItemSelected") +fun BottomNavigationView.bindOnNavigationItemSelectedListener( + listener: NavigationBarView.OnItemSelectedListener, +) { + this.setOnItemSelectedListener(listener) +} diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainViewModel.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainViewModel.kt index 6d9b454ba..d5ac8e0fe 100644 --- a/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainViewModel.kt +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/main/MainViewModel.kt @@ -1,5 +1,38 @@ package com.ddangddangddang.android.feature.main +import android.view.MenuItem +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.ddangddangddang.android.R -class MainViewModel : ViewModel() +class MainViewModel : ViewModel() { + private val _currentFragmentType: MutableLiveData = + MutableLiveData(FragmentType.HOME) + val currentFragmentType: LiveData + get() = _currentFragmentType + + fun setCurrentFragment(item: MenuItem): Boolean { + val menuItemId = item.itemId + val pageType = getPageType(menuItemId) + changeCurrentFragmentType(pageType) + + return true + } + + private fun getPageType(menuItemId: Int): FragmentType { + return when (menuItemId) { + R.id.menu_item_home -> FragmentType.HOME + R.id.menu_item_search -> FragmentType.SEARCH + R.id.menu_item_message -> FragmentType.MESSAGE + R.id.menu_item_my_page -> FragmentType.MY_PAGE + else -> throw IllegalArgumentException("Not found menu item") + } + } + + private fun changeCurrentFragmentType(fragmentType: FragmentType) { + if (currentFragmentType.value == fragmentType) return + + _currentFragmentType.value = fragmentType + } +} diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/message/MessageFragment.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/message/MessageFragment.kt new file mode 100644 index 000000000..a7c50e08f --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/message/MessageFragment.kt @@ -0,0 +1,7 @@ +package com.ddangddangddang.android.feature.message + +import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.FragmentMessageBinding +import com.ddangddangddang.android.util.binding.BindingFragment + +class MessageFragment : BindingFragment(R.layout.fragment_message) diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/mypage/MyPageFragment.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/mypage/MyPageFragment.kt new file mode 100644 index 000000000..a5ae29a7f --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/mypage/MyPageFragment.kt @@ -0,0 +1,7 @@ +package com.ddangddangddang.android.feature.mypage + +import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.FragmentMyPageBinding +import com.ddangddangddang.android.util.binding.BindingFragment + +class MyPageFragment : BindingFragment(R.layout.fragment_my_page) diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/search/SearchFragment.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/search/SearchFragment.kt new file mode 100644 index 000000000..e999ec006 --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/search/SearchFragment.kt @@ -0,0 +1,7 @@ +package com.ddangddangddang.android.feature.search + +import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.FragmentSearchBinding +import com.ddangddangddang.android.util.binding.BindingFragment + +class SearchFragment : BindingFragment(R.layout.fragment_search) diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index d0190c7be..fa23a129b 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -1,16 +1,38 @@ - + xmlns:tools="http://schemas.android.com/tools"> - - \ No newline at end of file + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_home.xml b/android/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 000000000..03c18d2f2 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_message.xml b/android/app/src/main/res/layout/fragment_message.xml new file mode 100644 index 000000000..e15be5f80 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_message.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_my_page.xml b/android/app/src/main/res/layout/fragment_my_page.xml new file mode 100644 index 000000000..06cf05ecf --- /dev/null +++ b/android/app/src/main/res/layout/fragment_my_page.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/fragment_search.xml b/android/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 000000000..2d38ad217 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/menu/menu_bottom_navigation.xml b/android/app/src/main/res/menu/menu_bottom_navigation.xml index 5db3bdffe..70faedf5c 100644 --- a/android/app/src/main/res/menu/menu_bottom_navigation.xml +++ b/android/app/src/main/res/menu/menu_bottom_navigation.xml @@ -1,9 +1,9 @@

+ android:title="@string/bottom_navigation_home" /> DdangDdangDdang - 메인 + 검색 쪽지 마이페이지 From 59860ef6bf7d9a91af5c37d239f1ff2649a34db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EB=AF=B8?= <63184334+JJ503@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:40:04 +0900 Subject: [PATCH 015/180] =?UTF-8?q?feat:=20#11=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20?= =?UTF-8?q?=EB=A0=88=ED=8F=AC=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 카테고리 엔티티 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * fix: DB 설정 문제 해결 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * refactor: 필요 없는 클래스 제거 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: Equals&HashCode 재정의 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 카테고리 레포지토리 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * refactor: 카테고리 연관관계 메서드 이름 변경 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * style: 컨벤션 적용 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * test: 유틸리티 어노테이션 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --------- Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --- .../com/ddang/ddang/DdangApplication.java | 7 +- .../java/com/ddang/ddang/LogController.java | 20 ------ .../ddang/ddang/category/domain/Category.java | 54 ++++++++++++++ .../persistence/JpaCategoryRepository.java | 13 ++++ .../src/main/resources/application-local.yml | 15 ++-- .../ddang/category/domain/CategoryTest.java | 29 ++++++++ .../JpaCategoryRepositoryTest.java | 70 +++++++++++++++++++ 7 files changed, 175 insertions(+), 33 deletions(-) delete mode 100644 backend/ddang/src/main/java/com/ddang/ddang/LogController.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepository.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java diff --git a/backend/ddang/src/main/java/com/ddang/ddang/DdangApplication.java b/backend/ddang/src/main/java/com/ddang/ddang/DdangApplication.java index 6a709615d..ce3ee40e1 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/DdangApplication.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/DdangApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class DdangApplication { - public static void main(String[] args) { - SpringApplication.run(DdangApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(DdangApplication.class, args); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/LogController.java b/backend/ddang/src/main/java/com/ddang/ddang/LogController.java deleted file mode 100644 index b46f570a4..000000000 --- a/backend/ddang/src/main/java/com/ddang/ddang/LogController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.ddang.ddang; - -import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@Slf4j -@RestController -@RequestMapping("/log") -public class LogController { - - @GetMapping - public void log() { - log.debug("debug"); - log.info("info"); - log.warn("warn"); - log.error("error"); - } -} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java b/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java new file mode 100644 index 000000000..a5d38b1bf --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java @@ -0,0 +1,54 @@ +package com.ddang.ddang.category.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "categories") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = "id") +@ToString(exclude = {"subCategories", "mainCategory"}) +public class Category { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 30) + private String name; + + @OneToMany(mappedBy = "mainCategory", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + private List subCategories = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "main_category_id", foreignKey = @ForeignKey(name = "fk_main_sub")) + private Category mainCategory; + + public Category(final String name) { + this.name = name; + } + + public void addSubCategory(final Category subCategory) { + subCategories.add(subCategory); + subCategory.mainCategory = this; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepository.java new file mode 100644 index 000000000..2c5ed006c --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepository.java @@ -0,0 +1,13 @@ +package com.ddang.ddang.category.infrastructure.persistence; + +import com.ddang.ddang.category.domain.Category; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface JpaCategoryRepository extends JpaRepository { + + List findMainAllByMainCategoryIsNull(); + + List findSubAllByMainCategoryId(Long mainCategoryId); +} diff --git a/backend/ddang/src/main/resources/application-local.yml b/backend/ddang/src/main/resources/application-local.yml index 9a58b20ea..a1e8a857a 100644 --- a/backend/ddang/src/main/resources/application-local.yml +++ b/backend/ddang/src/main/resources/application-local.yml @@ -1,7 +1,8 @@ spring: datasource: - url: jdbc:h2:tcp://localhost/~/test;MODE=MYSQL + url: jdbc:h2:tcp://localhost/~/test username: sa + driver-class-name: org.h2.Driver jpa: hibernate: @@ -9,14 +10,10 @@ spring: properties: hibernate: format_sql: true - dialect: org.hibernate.dialect.MySQL57Dialect - dialect.storage_engine: innodb + dialect: org.hibernate.dialect.H2Dialect + show_sql: true + use_sql_comments: true + open-in-view: false flyway: enabled: false - -logging: - level: - org.hibernate.sql: debug - - diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java new file mode 100644 index 000000000..b6b475bf1 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/domain/CategoryTest.java @@ -0,0 +1,29 @@ +package com.ddang.ddang.category.domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CategoryTest { + + @Test + void 카테고리_연관_관계를_세팅한다() { + // given + Category main = new Category("main"); + Category sub = new Category("sub"); + + // when + main.addSubCategory(sub); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(main.getSubCategories()) + .hasSize(1); + softAssertions.assertThat(sub.getMainCategory()) + .isNotNull(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java new file mode 100644 index 000000000..c3e267fe3 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/infrastructure/persistence/JpaCategoryRepositoryTest.java @@ -0,0 +1,70 @@ +package com.ddang.ddang.category.infrastructure.persistence; + +import com.ddang.ddang.category.domain.Category; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaCategoryRepositoryTest { + + @PersistenceContext + EntityManager em; + + @Autowired + JpaCategoryRepository categoryRepository; + + @Test + void 모든_메인_카테고리를_조회한다() { + // given + final Category main1 = new Category("main1"); + final Category main2 = new Category("main2"); + final Category sub = new Category("sub"); + + main1.addSubCategory(sub); + + categoryRepository.save(main1); + categoryRepository.save(main2); + + em.flush(); + em.clear(); + + // when + final List actual = categoryRepository.findMainAllByMainCategoryIsNull(); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다() { + // given + final Category main = new Category("main"); + final Category sub1 = new Category("sub1"); + final Category sub2 = new Category("sub2"); + + main.addSubCategory(sub1); + main.addSubCategory(sub2); + + categoryRepository.save(main); + + em.flush(); + em.clear(); + + // when + final List actual = categoryRepository.findSubAllByMainCategoryId(main.getId()); + + // then + assertThat(actual).hasSize(2); + } +} From 0bb468a17929b38157011ecf170ff2ad1dd2fc4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EB=AF=B8?= <63184334+JJ503@users.noreply.github.com> Date: Mon, 17 Jul 2023 18:13:12 +0900 Subject: [PATCH 016/180] =?UTF-8?q?feat:=20#12=20=EC=B9=B4=ED=85=8C?= =?UTF-8?q?=EA=B3=A0=EB=A6=AC=20api=20=EC=B6=94=EA=B0=80=20(#23)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 카테고리 서비스 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 카테고리 api 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 카테고리 조회 예외 처리 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 카테고리 api 예외 처리 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * docs: 카테고리 api 문서화 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * style: final 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * style: final 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --------- Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --- backend/ddang/src/docs/asciidoc/docs.adoc | 49 + .../category/application/CategoryService.java | 43 + .../application/dto/ReadCategoryDto.java | 10 + .../exception/CategoryNotFoundException.java | 8 + .../presentation/CategoryController.java | 44 + .../dto/ReadCategoriesResponse.java | 6 + .../dto/ReadCategoryResponse.java | 10 + .../exception/GlobalExceptionHandler.java | 22 +- .../exception/dto/ExceptionResponse.java | 4 + .../src/main/resources/static/docs/docs.html | 2307 +++++++++++++++++ .../application/CategoryServiceTest.java | 89 + .../presentation/CategoryControllerTest.java | 158 ++ 12 files changed, 2745 insertions(+), 5 deletions(-) create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/application/dto/ReadCategoryDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/application/exception/CategoryNotFoundException.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoriesResponse.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoryResponse.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/exception/dto/ExceptionResponse.java create mode 100644 backend/ddang/src/main/resources/static/docs/docs.html create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java diff --git a/backend/ddang/src/docs/asciidoc/docs.adoc b/backend/ddang/src/docs/asciidoc/docs.adoc index e69de29bb..a6cd4b4ca 100644 --- a/backend/ddang/src/docs/asciidoc/docs.adoc +++ b/backend/ddang/src/docs/asciidoc/docs.adoc @@ -0,0 +1,49 @@ += 땅땅땅 API 문서 +:toc: left +:source-highlighter: highlightjs +:sectlinks: + +[[overview-http-status-codes]] +=== HTTP status codes + +|=== +| 상태 코드 | 설명 +| `200 OK` +| 성공 +| `201 Created` +| 리소스 생성 +| `400 Bad Request` +| 잘못된 요청 +| `401 Unauthorized` +| 비인증 상태 +| `403 Forbidden` +| 권한 거부 +| `404 Not Found` +| 존재하지 않는 리소스에 대한 요청 +| `500 Internal Server Error` +| 서버 에러 +|=== + +== 카테고리 API + +=== 메인 카테고리 조회 + +==== 요청 + +include::{snippets}/category-controller-test/모든_메인_카테고리를_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/category-controller-test/모든_메인_카테고리를_조회한다/response-body.adoc[] +include::{snippets}/category-controller-test/모든_메인_카테고리를_조회한다/response-fields.adoc[] + +=== 서브 카테고리 조회 + +==== 요청 + +include::{snippets}/category-controller-test/메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다/http-request.adoc[] + +==== 응답 + +include::{snippets}/category-controller-test/메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다/response-body.adoc[] +include::{snippets}/category-controller-test/메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다/response-fields.adoc[] diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java b/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java new file mode 100644 index 000000000..bb3dc4c0b --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java @@ -0,0 +1,43 @@ +package com.ddang.ddang.category.application; + +import com.ddang.ddang.category.application.dto.ReadCategoryDto; +import com.ddang.ddang.category.application.exception.CategoryNotFoundException; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CategoryService { + + private final JpaCategoryRepository categoryRepository; + + public List readAllMain() { + final List mainCategories = categoryRepository.findMainAllByMainCategoryIsNull(); + + if (mainCategories.isEmpty()) { + throw new CategoryNotFoundException("등록된 메인 카테고리가 없습니다."); + } + + return mainCategories.stream() + .map(ReadCategoryDto::from) + .toList(); + } + + public List readAllSubByMainId(final Long mainId) { + final List subCategories = categoryRepository.findSubAllByMainCategoryId(mainId); + + if (subCategories.isEmpty()) { + throw new CategoryNotFoundException("지정한 메인 카테고리에 해당 서브 카테고리가 없습니다."); + } + + return subCategories.stream() + .map(ReadCategoryDto::from) + .toList(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/application/dto/ReadCategoryDto.java b/backend/ddang/src/main/java/com/ddang/ddang/category/application/dto/ReadCategoryDto.java new file mode 100644 index 000000000..d53026af6 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/application/dto/ReadCategoryDto.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.category.application.dto; + +import com.ddang.ddang.category.domain.Category; + +public record ReadCategoryDto(Long id, String name) { + + public static ReadCategoryDto from(final Category category) { + return new ReadCategoryDto(category.getId(), category.getName()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/application/exception/CategoryNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/category/application/exception/CategoryNotFoundException.java new file mode 100644 index 000000000..13d2faeaa --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/application/exception/CategoryNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.category.application.exception; + +public class CategoryNotFoundException extends IllegalArgumentException { + + public CategoryNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java new file mode 100644 index 000000000..499e26419 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/CategoryController.java @@ -0,0 +1,44 @@ +package com.ddang.ddang.category.presentation; + +import com.ddang.ddang.category.application.CategoryService; +import com.ddang.ddang.category.application.dto.ReadCategoryDto; +import com.ddang.ddang.category.presentation.dto.ReadCategoriesResponse; +import com.ddang.ddang.category.presentation.dto.ReadCategoryResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/categories") +@RequiredArgsConstructor +public class CategoryController { + + private final CategoryService categoryService; + + @GetMapping + public ResponseEntity readAllMain() { + final List readCategoryDtos = categoryService.readAllMain(); + final List readCategoryResponses = readCategoryDtos.stream() + .map(ReadCategoryResponse::from) + .toList(); + final ReadCategoriesResponse response = new ReadCategoriesResponse(readCategoryResponses); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{mainId}") + public ResponseEntity readAllSub(@PathVariable final Long mainId) { + final List readCategoryDtos = categoryService.readAllSubByMainId(mainId); + final List readCategoryResponses = readCategoryDtos.stream() + .map(ReadCategoryResponse::from) + .toList(); + final ReadCategoriesResponse response = new ReadCategoriesResponse(readCategoryResponses); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoriesResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoriesResponse.java new file mode 100644 index 000000000..6b2a1a130 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoriesResponse.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.category.presentation.dto; + +import java.util.List; + +public record ReadCategoriesResponse(List categories) { +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoryResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoryResponse.java new file mode 100644 index 000000000..3b537f504 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/presentation/dto/ReadCategoryResponse.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.category.presentation.dto; + +import com.ddang.ddang.category.application.dto.ReadCategoryDto; + +public record ReadCategoryResponse(Long id, String name) { + + public static ReadCategoryResponse from(final ReadCategoryDto dto) { + return new ReadCategoryResponse(dto.id(), dto.name()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index ef4f2abee..09e36bf47 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -1,8 +1,12 @@ package com.ddang.ddang.exception; +import com.ddang.ddang.category.application.exception.CategoryNotFoundException; +import com.ddang.ddang.exception.dto.ExceptionResponse; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; @@ -14,15 +18,23 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { @Override protected ResponseEntity handleExceptionInternal( - Exception ex, - Object body, - HttpHeaders headers, - HttpStatusCode statusCode, - WebRequest request + final Exception ex, + final Object body, + final HttpHeaders headers, + final HttpStatusCode statusCode, + final WebRequest request ) { logger.error(String.format(EXCEPTION_FORMAT, ex.getClass() .getSimpleName()), ex); return super.handleExceptionInternal(ex, body, headers, statusCode, request); } + + @ExceptionHandler(CategoryNotFoundException.class) + public ResponseEntity handleCategoryNotFoundException(final CategoryNotFoundException ex) { + logger.warn(String.format(EXCEPTION_FORMAT, CategoryNotFoundException.class), ex); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/dto/ExceptionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/dto/ExceptionResponse.java new file mode 100644 index 000000000..0e7911a47 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/dto/ExceptionResponse.java @@ -0,0 +1,4 @@ +package com.ddang.ddang.exception.dto; + +public record ExceptionResponse(String message) { +} diff --git a/backend/ddang/src/main/resources/static/docs/docs.html b/backend/ddang/src/main/resources/static/docs/docs.html new file mode 100644 index 000000000..46b4c4f0c --- /dev/null +++ b/backend/ddang/src/main/resources/static/docs/docs.html @@ -0,0 +1,2307 @@ + + + + + + + + 땅땅땅 API 문서 + + + + + + +
+
+

HTTP status codes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

상태 코드

설명

200 OK

성공

201 Created

리소스 생성

400 Bad Request

잘못된 요청

401 Unauthorized

비인증 상태

403 Forbidden

권한 거부

404 Not Found

존재하지 않는 리소스에 대한 요청

500 Internal Server Error

+

서버 에러

+
+
+

카테고리 API

+
+
+

메인 카테고리 조회

+
+

요청

+
+
+
GET /categories HTTP/1.1
+Content-Type: application/json
+
+
+
+
+

응답

+
+
+
{
+  "categories" : [ {
+    "id" : 1,
+    "name" : "main1"
+  }, {
+    "id" : 2,
+    "name" : "main2"
+  } ]
+}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

categories.[].id

Number

메인 카테고리 ID

categories.[].name

+

String

메인 카테고리 이름

+
+
+
+

서브 카테고리 조회

+
+

요청

+
+
+
GET /categories/1 HTTP/1.1
+Content-Type: application/json
+
+
+
+
+

응답

+
+
+
{
+  "categories" : [ {
+    "id" : 2,
+    "name" : "sub1"
+  }, {
+    "id" : 3,
+    "name" : "sub2"
+  } ]
+}
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
PathTypeDescription

categories.[].id

Number

서브 카테고리 ID

categories.[].name

+

String

서브 카테고리 이름

+
+
+
+
+
+ + + + + diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java new file mode 100644 index 000000000..ddcb2bcb0 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java @@ -0,0 +1,89 @@ +package com.ddang.ddang.category.application; + +import com.ddang.ddang.category.application.dto.ReadCategoryDto; +import com.ddang.ddang.category.application.exception.CategoryNotFoundException; +import com.ddang.ddang.category.domain.Category; +import com.ddang.ddang.category.infrastructure.persistence.JpaCategoryRepository; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CategoryServiceTest { + + @Autowired + CategoryService categoryService; + + @Autowired + JpaCategoryRepository categoryRepository; + + @Test + void 모든_메인_카테고리를_조회한다() { + // given + final Category main1 = new Category("main1"); + final Category main2 = new Category("main2"); + final Category sub = new Category("sub"); + + main1.addSubCategory(sub); + + categoryRepository.save(main1); + categoryRepository.save(main2); + + // when + final List actual = categoryService.readAllMain(); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 메인_카테고리가_없는_경우_메인_카테고리_조회시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> categoryService.readAllMain()) + .isInstanceOf(CategoryNotFoundException.class) + .hasMessage("등록된 메인 카테고리가 없습니다."); + } + + @Test + void 메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다() { + // given + final Category main = new Category("main"); + final Category sub1 = new Category("sub1"); + final Category sub2 = new Category("sub2"); + + main.addSubCategory(sub1); + main.addSubCategory(sub2); + + categoryRepository.save(main); + + // when + final List actual = categoryService.readAllSubByMainId(main.getId()); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 지정한_메인_카테고리에_해당_서브_카테고리가_없는_경우_서브_카테고리_조회시_예외가_발생한다() { + // given + final Category main = new Category("main"); + + categoryRepository.save(main); + + // when & then + assertThatThrownBy(() -> categoryService.readAllSubByMainId(main.getId())) + .isInstanceOf(CategoryNotFoundException.class) + .hasMessage("지정한 메인 카테고리에 해당 서브 카테고리가 없습니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java new file mode 100644 index 000000000..18799620c --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/presentation/CategoryControllerTest.java @@ -0,0 +1,158 @@ +package com.ddang.ddang.category.presentation; + +import com.ddang.ddang.category.application.CategoryService; +import com.ddang.ddang.category.application.dto.ReadCategoryDto; +import com.ddang.ddang.category.application.exception.CategoryNotFoundException; +import com.ddang.ddang.configuration.RestDocsConfiguration; +import com.ddang.ddang.exception.GlobalExceptionHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.restdocs.RestDocumentationContextProvider; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; +import org.springframework.restdocs.payload.JsonFieldType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = {CategoryController.class}) +@AutoConfigureRestDocs +@Import(RestDocsConfiguration.class) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class CategoryControllerTest { + + @MockBean + CategoryService categoryService; + + @Autowired + CategoryController categoryController; + + @Autowired + RestDocumentationResultHandler restDocs; + + MockMvc mockMvc; + + @BeforeEach + void setUp(@Autowired RestDocumentationContextProvider provider) { + mockMvc = MockMvcBuilders.standaloneSetup(categoryController) + .setControllerAdvice(new GlobalExceptionHandler()) + .apply(MockMvcRestDocumentation.documentationConfiguration(provider)) + .alwaysDo(print()) + .alwaysDo(restDocs) + .build(); + } + + @Test + void 모든_메인_카테고리를_조회한다() throws Exception { + // given + final ReadCategoryDto main1 = new ReadCategoryDto(1L, "main1"); + final ReadCategoryDto main2 = new ReadCategoryDto(2L, "main2"); + + given(categoryService.readAllMain()).willReturn(List.of(main1, main2)); + + // when & then + mockMvc.perform(get("/categories") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.categories.[0].id", is(main1.id()), Long.class), + jsonPath("$.categories.[0].name", is(main1.name())), + jsonPath("$.categories.[1].id", is(main2.id()), Long.class), + jsonPath("$.categories.[1].name", is(main2.name())) + ) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("categories.[].id").type(JsonFieldType.NUMBER) + .description("메인 카테고리 ID"), + fieldWithPath("categories.[].name").type(JsonFieldType.STRING) + .description("메인 카테고리 이름") + ) + ) + ); + } + + @Test + void 메인_카테고리가_없는_경우_메인_카테고리_조회시_404를_반환한다() throws Exception { + // given + final CategoryNotFoundException categoryNotFoundException = new CategoryNotFoundException("등록된 메인 카테고리가 없습니다."); + given(categoryService.readAllMain()).willThrow(categoryNotFoundException); + + // when & then + mockMvc.perform(get("/categories") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message", is(categoryNotFoundException.getMessage())) + ); + } + + @Test + void 메인_카테고리에_해당하는_모든_서브_카테고리를_조회한다() throws Exception { + // given + final ReadCategoryDto main = new ReadCategoryDto(1L, "main"); + final ReadCategoryDto sub1 = new ReadCategoryDto(2L, "sub1"); + final ReadCategoryDto sub2 = new ReadCategoryDto(3L, "sub2"); + + given(categoryService.readAllSubByMainId(main.id())).willReturn(List.of(sub1, sub2)); + + // when & then + mockMvc.perform(get("/categories/{mainId}", main.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.categories.[0].id", is(sub1.id()), Long.class), + jsonPath("$.categories.[0].name", is(sub1.name())), + jsonPath("$.categories.[1].id", is(sub2.id()), Long.class), + jsonPath("$.categories.[1].name", is(sub2.name())) + ) + .andDo( + restDocs.document( + responseFields( + fieldWithPath("categories.[].id").type(JsonFieldType.NUMBER) + .description("서브 카테고리 ID"), + fieldWithPath("categories.[].name").type(JsonFieldType.STRING) + .description("서브 카테고리 이름") + ) + ) + ); + } + + @Test + void 지정한_메인_카테고리에_해당_서브_카테고리가_없는_경우_서브_카테고리_조회시_404를_반환한다() throws Exception { + // given + final ReadCategoryDto main = new ReadCategoryDto(1L, "main"); + + final CategoryNotFoundException categoryNotFoundException = + new CategoryNotFoundException("지정한 메인 카테고리에 해당 서브 카테고리가 없습니다."); + + given(categoryService.readAllSubByMainId(main.id())).willThrow(categoryNotFoundException); + + // when & then + mockMvc.perform(get("/categories/{mainId}", main.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message", is(categoryNotFoundException.getMessage())) + ); + } +} From f5af12ef1628f7b06d98c89f04072c1e3376f2ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EB=AF=B8?= <63184334+JJ503@users.noreply.github.com> Date: Mon, 17 Jul 2023 20:34:37 +0900 Subject: [PATCH 017/180] =?UTF-8?q?feat:=20#13=20=EC=A7=80=EC=97=AD=20?= =?UTF-8?q?=EC=97=94=ED=8B=B0=ED=8B=B0=20=EB=B0=8F=20=EB=A0=88=ED=8F=AC?= =?UTF-8?q?=EC=A7=80=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 외래키 제약조건 이름 수정 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 지역 엔티티 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 지역 레포지토리 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --------- Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --- .../ddang/ddang/category/domain/Category.java | 2 +- .../com/ddang/ddang/region/domain/Region.java | 65 +++++++++++++ .../persistence/JpaRegionRepository.java | 19 ++++ .../ddang/ddang/region/domain/RegionTest.java | 52 ++++++++++ .../persistence/JpaRegionRepositoryTest.java | 94 +++++++++++++++++++ 5 files changed, 231 insertions(+), 1 deletion(-) create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/domain/Region.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java b/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java index a5d38b1bf..758e83007 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/domain/Category.java @@ -40,7 +40,7 @@ public class Category { private List subCategories = new ArrayList<>(); @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "main_category_id", foreignKey = @ForeignKey(name = "fk_main_sub")) + @JoinColumn(name = "main_category_id", foreignKey = @ForeignKey(name = "fk_category_main_sub")) private Category mainCategory; public Category(final String name) { diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/domain/Region.java b/backend/ddang/src/main/java/com/ddang/ddang/region/domain/Region.java new file mode 100644 index 000000000..3714234d4 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/domain/Region.java @@ -0,0 +1,65 @@ +package com.ddang.ddang.region.domain; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@EqualsAndHashCode(of = {"id"}) +@ToString(exclude = {"secondRegions", "thirdRegions"}) +public class Region { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(length = 30) + private String name; + + @OneToMany(mappedBy = "firstRegion", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + private List secondRegions = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "first_region_id", foreignKey = @ForeignKey(name = "fk_region_first_second")) + private Region firstRegion; + + @OneToMany(mappedBy = "secondRegion", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + private List thirdRegions = new ArrayList<>(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "second_region_id", foreignKey = @ForeignKey(name = "fk_region_second_third")) + private Region secondRegion; + + public Region(final String name) { + this.name = name; + } + + public void addSecondRegion(final Region secondRegion) { + this.secondRegions.add(secondRegion); + secondRegion.firstRegion = this; + } + + public void addThirdRegion(final Region thirdRegion) { + this.thirdRegions.add(thirdRegion); + thirdRegion.secondRegion = this; + thirdRegion.firstRegion = this.firstRegion; + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java b/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java new file mode 100644 index 000000000..ec5347118 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepository.java @@ -0,0 +1,19 @@ +package com.ddang.ddang.region.infrastructure.persistence; + +import com.ddang.ddang.region.domain.Region; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface JpaRegionRepository extends JpaRepository { + + @Query("select r from Region r where r.firstRegion.id is null and r.secondRegion.id is null") + List findFirstAll(); + + @Query("select r from Region r where r.firstRegion.id = :firstRegionId and r.secondRegion.id is null") + List findSecondAllByFirstRegionId(final Long firstRegionId); + + @Query("select r from Region r where r.firstRegion.id = :firstRegionId and r.secondRegion.id = :secondRegionId") + List findThirdAllByFirstAndSecondRegionId(final Long firstRegionId, final Long secondRegionId); +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java new file mode 100644 index 000000000..ace3aba71 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/domain/RegionTest.java @@ -0,0 +1,52 @@ +package com.ddang.ddang.region.domain; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RegionTest { + + @Test + void 첫번째_지역과_두번째_지역의_연관관계를_세팅한다() { + // given + final Region first = new Region("서울특별시"); + final Region second = new Region("강남구"); + + // when + first.addSecondRegion(second); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(first.getSecondRegions()) + .hasSize(1); + softAssertions.assertThat(second.getFirstRegion()) + .isNotNull(); + }); + } + + @Test + void 두번째_지역과_세번째_지역의_연관관계를_세팅한다() { + // given + final Region first = new Region("서울특별시"); + final Region second = new Region("강남구"); + final Region third = new Region("역삼동"); + + first.addSecondRegion(second); + + // when + second.addThirdRegion(third); + + // then + SoftAssertions.assertSoftly(softAssertions -> { + softAssertions.assertThat(second.getThirdRegions()) + .hasSize(1); + softAssertions.assertThat(third.getSecondRegion()) + .isNotNull(); + softAssertions.assertThat(third.getFirstRegion()) + .isNotNull(); + }); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java new file mode 100644 index 000000000..0b69771a8 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/infrastructure/persistence/JpaRegionRepositoryTest.java @@ -0,0 +1,94 @@ +package com.ddang.ddang.region.infrastructure.persistence; + +import com.ddang.ddang.region.domain.Region; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class JpaRegionRepositoryTest { + + @PersistenceContext + EntityManager em; + + @Autowired + JpaRegionRepository regionRepository; + + @Test + void 모든_첫번째_지역을_조회한다() { + // given + final Region first1 = new Region("first1"); + final Region first2 = new Region("first2"); + final Region second = new Region("second"); + + first1.addSecondRegion(second); + + regionRepository.save(first1); + regionRepository.save(first2); + + em.flush(); + em.clear(); + + // when + final List actual = regionRepository.findFirstAll(); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() { + // given + final Region first = new Region("first"); + final Region second1 = new Region("second1"); + final Region second2 = new Region("second2"); + + first.addSecondRegion(second1); + first.addSecondRegion(second2); + + regionRepository.save(first); + + em.flush(); + em.clear(); + + // when + final List actual = regionRepository.findSecondAllByFirstRegionId(first.getId()); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() { + // given + final Region first = new Region("first"); + final Region second = new Region("second"); + final Region third1 = new Region("third1"); + final Region third2 = new Region("third2"); + + first.addSecondRegion(second); + second.addThirdRegion(third1); + second.addThirdRegion(third2); + + regionRepository.save(first); + + em.flush(); + em.clear(); + + // when + final List actual = regionRepository.findThirdAllByFirstAndSecondRegionId(first.getId(), second.getId()); + + // then + assertThat(actual).hasSize(2); + } +} From 5b993420e668fa1116013e43c71634503d8f23f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=9C=EC=9D=B4=EB=AF=B8?= <63184334+JJ503@users.noreply.github.com> Date: Tue, 18 Jul 2023 11:44:37 +0900 Subject: [PATCH 018/180] =?UTF-8?q?feat:=20#14=20=EC=A7=80=EC=97=AD=20api?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#32)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style: 예외 메시지 수정 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 지역 서비스 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 * feat: 지역 api 추가 Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --------- Co-authored-by: swonny Co-authored-by: apptie Co-authored-by: kwonyj1022 --- .../category/application/CategoryService.java | 2 +- .../exception/GlobalExceptionHandler.java | 9 + .../region/application/RegionService.java | 59 +++++++ .../region/application/dto/ReadRegionDto.java | 10 ++ .../exception/RegionNotFoundException.java | 8 + .../region/presentation/RegionController.java | 61 +++++++ .../presentation/dto/ReadRegionResponse.java | 10 ++ .../presentation/dto/ReadRegionsResponse.java | 6 + .../application/CategoryServiceTest.java | 4 +- .../region/application/RegionServiceTest.java | 126 +++++++++++++ .../presentation/RegionControllerTest.java | 167 ++++++++++++++++++ 11 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/application/RegionService.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/application/dto/ReadRegionDto.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/application/exception/RegionNotFoundException.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionResponse.java create mode 100644 backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionsResponse.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java create mode 100644 backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java diff --git a/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java b/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java index bb3dc4c0b..48df85d4a 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/category/application/CategoryService.java @@ -33,7 +33,7 @@ public List readAllSubByMainId(final Long mainId) { final List subCategories = categoryRepository.findSubAllByMainCategoryId(mainId); if (subCategories.isEmpty()) { - throw new CategoryNotFoundException("지정한 메인 카테고리에 해당 서브 카테고리가 없습니다."); + throw new CategoryNotFoundException("지정한 메인 카테고리에 해당하는 서브 카테고리가 없습니다."); } return subCategories.stream() diff --git a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java index 09e36bf47..7090ed7bd 100644 --- a/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java +++ b/backend/ddang/src/main/java/com/ddang/ddang/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import com.ddang.ddang.category.application.exception.CategoryNotFoundException; import com.ddang.ddang.exception.dto.ExceptionResponse; +import com.ddang.ddang.region.application.exception.RegionNotFoundException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; @@ -37,4 +38,12 @@ public ResponseEntity handleCategoryNotFoundException(final C return ResponseEntity.status(HttpStatus.NOT_FOUND) .body(new ExceptionResponse(ex.getMessage())); } + + @ExceptionHandler(RegionNotFoundException.class) + public ResponseEntity handleRegionNotFoundException(final RegionNotFoundException ex) { + logger.warn(String.format(EXCEPTION_FORMAT, RegionNotFoundException.class), ex); + + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ExceptionResponse(ex.getMessage())); + } } diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/application/RegionService.java b/backend/ddang/src/main/java/com/ddang/ddang/region/application/RegionService.java new file mode 100644 index 000000000..3f41ac96e --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/application/RegionService.java @@ -0,0 +1,59 @@ +package com.ddang.ddang.region.application; + +import com.ddang.ddang.region.application.dto.ReadRegionDto; +import com.ddang.ddang.region.application.exception.RegionNotFoundException; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class RegionService { + + private final JpaRegionRepository regionRepository; + + public List readAllFirst() { + final List firstRegions = regionRepository.findFirstAll(); + + if (firstRegions.isEmpty()) { + throw new RegionNotFoundException("등록된 지역이 없습니다."); + } + + return firstRegions.stream() + .map(ReadRegionDto::from) + .toList(); + } + + public List readAllSecondByFirstRegionId(final Long firstRegionId) { + final List secondRegions = regionRepository.findSecondAllByFirstRegionId(firstRegionId); + + if (secondRegions.isEmpty()) { + throw new RegionNotFoundException("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다."); + } + + return secondRegions.stream() + .map(ReadRegionDto::from) + .toList(); + } + + public List readAllThirdByFirstAndSecondRegionId( + final Long firstRegionId, + final Long secondRegionId + ) { + final List thirdRegions = regionRepository.findThirdAllByFirstAndSecondRegionId( + firstRegionId, + secondRegionId + ); + + if (thirdRegions.isEmpty()) { + throw new RegionNotFoundException("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다."); + } + + return thirdRegions.stream() + .map(ReadRegionDto::from) + .toList(); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/application/dto/ReadRegionDto.java b/backend/ddang/src/main/java/com/ddang/ddang/region/application/dto/ReadRegionDto.java new file mode 100644 index 000000000..d15295c0d --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/application/dto/ReadRegionDto.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.region.application.dto; + +import com.ddang.ddang.region.domain.Region; + +public record ReadRegionDto(Long id, String name) { + + public static ReadRegionDto from(final Region region) { + return new ReadRegionDto(region.getId(), region.getName()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/application/exception/RegionNotFoundException.java b/backend/ddang/src/main/java/com/ddang/ddang/region/application/exception/RegionNotFoundException.java new file mode 100644 index 000000000..e4527376f --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/application/exception/RegionNotFoundException.java @@ -0,0 +1,8 @@ +package com.ddang.ddang.region.application.exception; + +public class RegionNotFoundException extends IllegalArgumentException { + + public RegionNotFoundException(final String message) { + super(message); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java new file mode 100644 index 000000000..e22709111 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/RegionController.java @@ -0,0 +1,61 @@ +package com.ddang.ddang.region.presentation; + +import com.ddang.ddang.region.application.RegionService; +import com.ddang.ddang.region.application.dto.ReadRegionDto; +import com.ddang.ddang.region.presentation.dto.ReadRegionResponse; +import com.ddang.ddang.region.presentation.dto.ReadRegionsResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/regions") +@RequiredArgsConstructor +public class RegionController { + + private final RegionService regionService; + + @GetMapping + public ResponseEntity readAllFirst() { + final List readRegionDtos = regionService.readAllFirst(); + final List readRegionResponses = readRegionDtos.stream() + .map(ReadRegionResponse::from) + .toList(); + final ReadRegionsResponse response = new ReadRegionsResponse(readRegionResponses); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{firstId}") + public ResponseEntity readAllSecond(@PathVariable final Long firstId) { + final List readRegionDtos = regionService.readAllSecondByFirstRegionId(firstId); + final List readRegionResponses = readRegionDtos.stream() + .map(ReadRegionResponse::from) + .toList(); + final ReadRegionsResponse response = new ReadRegionsResponse(readRegionResponses); + + return ResponseEntity.ok(response); + } + + @GetMapping("/{firstId}/{secondId}") + public ResponseEntity readAllSecond( + @PathVariable final Long firstId, + @PathVariable final Long secondId + ) { + final List readRegionDtos = regionService.readAllThirdByFirstAndSecondRegionId( + firstId, + secondId + ); + final List readRegionResponses = readRegionDtos.stream() + .map(ReadRegionResponse::from) + .toList(); + final ReadRegionsResponse response = new ReadRegionsResponse(readRegionResponses); + + return ResponseEntity.ok(response); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionResponse.java new file mode 100644 index 000000000..281310fdb --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionResponse.java @@ -0,0 +1,10 @@ +package com.ddang.ddang.region.presentation.dto; + +import com.ddang.ddang.region.application.dto.ReadRegionDto; + +public record ReadRegionResponse(Long id, String name) { + + public static ReadRegionResponse from(final ReadRegionDto dto) { + return new ReadRegionResponse(dto.id(), dto.name()); + } +} diff --git a/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionsResponse.java b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionsResponse.java new file mode 100644 index 000000000..b66a09117 --- /dev/null +++ b/backend/ddang/src/main/java/com/ddang/ddang/region/presentation/dto/ReadRegionsResponse.java @@ -0,0 +1,6 @@ +package com.ddang.ddang.region.presentation.dto; + +import java.util.List; + +public record ReadRegionsResponse(List regions) { +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java index ddcb2bcb0..5fe284f5d 100644 --- a/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java +++ b/backend/ddang/src/test/java/com/ddang/ddang/category/application/CategoryServiceTest.java @@ -75,7 +75,7 @@ class CategoryServiceTest { } @Test - void 지정한_메인_카테고리에_해당_서브_카테고리가_없는_경우_서브_카테고리_조회시_예외가_발생한다() { + void 지정한_메인_카테고리에_해당하는_서브_카테고리가_없는_경우_서브_카테고리_조회시_예외가_발생한다() { // given final Category main = new Category("main"); @@ -84,6 +84,6 @@ class CategoryServiceTest { // when & then assertThatThrownBy(() -> categoryService.readAllSubByMainId(main.getId())) .isInstanceOf(CategoryNotFoundException.class) - .hasMessage("지정한 메인 카테고리에 해당 서브 카테고리가 없습니다."); + .hasMessage("지정한 메인 카테고리에 해당하는 서브 카테고리가 없습니다."); } } diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java new file mode 100644 index 000000000..8b95bd46b --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/application/RegionServiceTest.java @@ -0,0 +1,126 @@ +package com.ddang.ddang.region.application; + +import com.ddang.ddang.region.application.dto.ReadRegionDto; +import com.ddang.ddang.region.application.exception.RegionNotFoundException; +import com.ddang.ddang.region.domain.Region; +import com.ddang.ddang.region.infrastructure.persistence.JpaRegionRepository; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Transactional +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RegionServiceTest { + + @Autowired + RegionService regionService; + + @Autowired + JpaRegionRepository regionRepository; + + @Test + void 모든_첫번째_지역을_조회한다() { + // given + final Region first1 = new Region("first1"); + final Region first2 = new Region("first2"); + final Region second = new Region("second"); + + first1.addSecondRegion(second); + + regionRepository.save(first1); + regionRepository.save(first2); + + // when + final List actual = regionService.readAllFirst(); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 첫번째_지역이_없는_경우_지역_조회시_예외가_발생한다() { + // when & then + assertThatThrownBy(() -> regionService.readAllFirst()) + .isInstanceOf(RegionNotFoundException.class) + .hasMessage("등록된 지역이 없습니다."); + } + + @Test + void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() { + // given + final Region first = new Region("first"); + final Region second1 = new Region("second1"); + final Region second2 = new Region("second2"); + + first.addSecondRegion(second1); + first.addSecondRegion(second2); + + regionRepository.save(first); + + // when + final List actual = regionService.readAllSecondByFirstRegionId(first.getId()); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 지정한_첫번째_지역에_해당하는_두번째_지역이_없는_경우_두번째_지역_조회시_예외가_발생한다() { + // given + final Region first = new Region("first"); + + regionRepository.save(first); + + // when & then + assertThatThrownBy(() -> regionService.readAllSecondByFirstRegionId(first.getId())) + .isInstanceOf(RegionNotFoundException.class) + .hasMessage("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다."); + } + + @Test + void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() { + // given + final Region first = new Region("first"); + final Region second = new Region("second"); + final Region third1 = new Region("third1"); + final Region third2 = new Region("third2"); + + first.addSecondRegion(second); + second.addThirdRegion(third1); + second.addThirdRegion(third2); + + regionRepository.save(first); + + // when + final List actual = regionService.readAllThirdByFirstAndSecondRegionId(first.getId(), second.getId()); + + // then + assertThat(actual).hasSize(2); + } + + @Test + void 지정한_첫번째와_두번째_지역에_해당하는_세번째_지역이_없는_경우_세번째_지역_조회시_예외가_발생한다() { + // given + final Region first = new Region("first"); + final Region second = new Region("second"); + + first.addSecondRegion(second); + + regionRepository.save(first); + + // when & then + assertThatThrownBy(() -> regionService.readAllThirdByFirstAndSecondRegionId(first.getId(), second.getId())) + .isInstanceOf(RegionNotFoundException.class) + .hasMessage("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다."); + } +} diff --git a/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java new file mode 100644 index 000000000..029903c56 --- /dev/null +++ b/backend/ddang/src/test/java/com/ddang/ddang/region/presentation/RegionControllerTest.java @@ -0,0 +1,167 @@ +package com.ddang.ddang.region.presentation; + +import com.ddang.ddang.exception.GlobalExceptionHandler; +import com.ddang.ddang.region.application.RegionService; +import com.ddang.ddang.region.application.dto.ReadRegionDto; +import com.ddang.ddang.region.application.exception.RegionNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; + +import static org.hamcrest.Matchers.is; +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(controllers = {RegionController.class}) +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@SuppressWarnings("NonAsciiCharacters") +class RegionControllerTest { + + @MockBean + RegionService regionService; + + @Autowired + RegionController regionController; + + MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(regionController) + .setControllerAdvice(new GlobalExceptionHandler()) + .alwaysDo(print()) + .build(); + } + + @Test + void 모든_첫번째_지역을_조회한다() throws Exception { + // given + + final ReadRegionDto first1 = new ReadRegionDto(1L, "first1"); + final ReadRegionDto first2 = new ReadRegionDto(2L, "first2"); + + given(regionService.readAllFirst()).willReturn(List.of(first1, first2)); + + // when & then + mockMvc.perform(get("/regions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.regions.[0].id", is(first1.id()), Long.class), + jsonPath("$.regions.[0].name", is(first1.name())), + jsonPath("$.regions.[1].id", is(first2.id()), Long.class), + jsonPath("$.regions.[1].name", is(first2.name())) + ); + } + + @Test + void 첫번째_지역이_없는_경우_첫번째_지역_조회시_404를_반환한다() throws Exception { + // given + final RegionNotFoundException regionNotFoundException = new RegionNotFoundException("등록된 지역이 없습니다."); + given(regionService.readAllFirst()).willThrow(regionNotFoundException); + + // when & then + mockMvc.perform(get("/regions") + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message", is(regionNotFoundException.getMessage())) + ); + } + + @Test + void 첫번째_지역에_해당하는_모든_두번째_지역을_조회한다() throws Exception { + // given + final ReadRegionDto first = new ReadRegionDto(1L, "first"); + final ReadRegionDto second1 = new ReadRegionDto(2L, "second1"); + final ReadRegionDto second2 = new ReadRegionDto(3L, "second2"); + + given(regionService.readAllSecondByFirstRegionId(first.id())).willReturn(List.of(second1, second2)); + + // when & then + mockMvc.perform(get("/regions/{firstId}", first.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.regions.[0].id", is(second1.id()), Long.class), + jsonPath("$.regions.[0].name", is(second1.name())), + jsonPath("$.regions.[1].id", is(second2.id()), Long.class), + jsonPath("$.regions.[1].name", is(second2.name())) + ); + } + + @Test + void 지정한_첫번째_지역에_해당하는_두번째_지역이_없는_경우_두번째_지역_조회시_404를_반환한다() throws Exception { + // given + final ReadRegionDto first = new ReadRegionDto(1L, "first"); + + final RegionNotFoundException regionNotFoundException = + new RegionNotFoundException("지정한 첫 번째 지역에 해당하는 두 번째 지역이 없습니다."); + + given(regionService.readAllSecondByFirstRegionId(first.id())).willThrow(regionNotFoundException); + + // when & then + mockMvc.perform(get("/regions/{firstId}", first.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message", is(regionNotFoundException.getMessage())) + ); + } + + @Test + void 두번째_지역에_해당하는_모든_세번째_지역을_조회한다() throws Exception { + // given + final ReadRegionDto first = new ReadRegionDto(1L, "first"); + final ReadRegionDto second = new ReadRegionDto(2L, "second"); + final ReadRegionDto third1 = new ReadRegionDto(3L, "third1"); + final ReadRegionDto third2 = new ReadRegionDto(3L, "third2"); + + given(regionService.readAllThirdByFirstAndSecondRegionId(first.id(), second.id())) + .willReturn(List.of(third1, third2)); + + // when & then + mockMvc.perform(get("/regions/{firstId}/{secondId}", first.id(), second.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isOk(), + jsonPath("$.regions.[0].id", is(third1.id()), Long.class), + jsonPath("$.regions.[0].name", is(third1.name())), + jsonPath("$.regions.[1].id", is(third2.id()), Long.class), + jsonPath("$.regions.[1].name", is(third2.name())) + ); + } + + @Test + void 지정한_첫번째와_두번째_지역에_해당하는_세번째_지역이_없는_경우_세번째_지역_조회시_404를_반환한다() throws Exception { + // given + final ReadRegionDto first = new ReadRegionDto(1L, "first"); + final ReadRegionDto second = new ReadRegionDto(2L, "second"); + + final RegionNotFoundException regionNotFoundException = + new RegionNotFoundException("지정한 첫 번째와 두 번째 지역에 해당하는 세 번째 지역이 없습니다."); + + given(regionService.readAllThirdByFirstAndSecondRegionId(first.id(), second.id())) + .willThrow(regionNotFoundException); + + // when & then + mockMvc.perform(get("/regions/{firstId}/{secondId}", first.id(), second.id()) + .contentType(MediaType.APPLICATION_JSON)) + .andExpectAll( + status().isNotFound(), + jsonPath("$.message", is(regionNotFoundException.getMessage())) + ); + } +} From 398d6db5ef1216afe5294c1bd378e6a88a1d7ea5 Mon Sep 17 00:00:00 2001 From: Song Hyemin Date: Tue, 18 Jul 2023 12:12:52 +0900 Subject: [PATCH 019/180] =?UTF-8?q?feat:=20#17=20=EA=B2=BD=EB=A7=A4=20?= =?UTF-8?q?=EC=83=81=ED=92=88=20=EB=93=B1=EB=A1=9D=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20MVVM=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=20=EC=84=A4=EA=B3=84=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: MVVM 구조 생성 * Feat: 레이아웃 작성 * refactor: 중복 함수 호출 삭제 * refactor: 런처 액티비티 수정 * refactor: 들여쓰기 추가 * refactor: string name 수정 --- android/app/src/main/AndroidManifest.xml | 5 +- .../feature/common/ViewModelFactory.kt | 2 + .../register/RegisterAuctionActivity.kt | 17 ++ .../register/RegisterAuctionViewModel.kt | 5 + .../drawable/bg_stroke_gray_radius_1dp.xml | 8 + .../main/res/drawable/ic_edittext_cursor.xml | 5 + .../res/layout/activity_register_auction.xml | 273 ++++++++++++++++++ android/app/src/main/res/values/strings.xml | 12 +- 8 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionActivity.kt create mode 100644 android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionViewModel.kt create mode 100644 android/app/src/main/res/drawable/bg_stroke_gray_radius_1dp.xml create mode 100644 android/app/src/main/res/drawable/ic_edittext_cursor.xml create mode 100644 android/app/src/main/res/layout/activity_register_auction.xml diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index c5494cfb5..7ec14d258 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -22,6 +22,9 @@ android:supportsRtl="true" android:theme="@style/Theme.DdangDdangDdang" tools:targetApi="31"> + @@ -33,4 +36,4 @@ - \ No newline at end of file + diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/common/ViewModelFactory.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/common/ViewModelFactory.kt index a204d9b10..fcf82f3e2 100644 --- a/android/app/src/main/java/com/ddangddangddang/android/feature/common/ViewModelFactory.kt +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/common/ViewModelFactory.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras import com.ddangddangddang.android.feature.main.MainViewModel +import com.ddangddangddang.android.feature.register.RegisterAuctionViewModel @Suppress("UNCHECKED_CAST") val viewModelFactory = object : ViewModelProvider.Factory { @@ -12,6 +13,7 @@ val viewModelFactory = object : ViewModelProvider.Factory { // 레포지토리 싱글톤 객체 얻어옴 when { isAssignableFrom(MainViewModel::class.java) -> MainViewModel() + isAssignableFrom(RegisterAuctionViewModel::class.java) -> RegisterAuctionViewModel() // 여기에 뷰모델 추가 로직 작성하면 됨 else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionActivity.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionActivity.kt new file mode 100644 index 000000000..c046500ca --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionActivity.kt @@ -0,0 +1,17 @@ +package com.ddangddangddang.android.feature.register + +import android.os.Bundle +import androidx.activity.viewModels +import com.ddangddangddang.android.R +import com.ddangddangddang.android.databinding.ActivityRegisterAuctionBinding +import com.ddangddangddang.android.feature.common.viewModelFactory +import com.ddangddangddang.android.util.binding.BindingActivity + +class RegisterAuctionActivity : BindingActivity(R.layout.activity_register_auction) { + private val viewModel by viewModels { viewModelFactory } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding.viewModel = viewModel + } +} diff --git a/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionViewModel.kt b/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionViewModel.kt new file mode 100644 index 000000000..7c5a35acf --- /dev/null +++ b/android/app/src/main/java/com/ddangddangddang/android/feature/register/RegisterAuctionViewModel.kt @@ -0,0 +1,5 @@ +package com.ddangddangddang.android.feature.register + +import androidx.lifecycle.ViewModel + +class RegisterAuctionViewModel : ViewModel() diff --git a/android/app/src/main/res/drawable/bg_stroke_gray_radius_1dp.xml b/android/app/src/main/res/drawable/bg_stroke_gray_radius_1dp.xml new file mode 100644 index 000000000..3b84fc1e3 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_stroke_gray_radius_1dp.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_edittext_cursor.xml b/android/app/src/main/res/drawable/ic_edittext_cursor.xml new file mode 100644 index 000000000..168b9a1b4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_edittext_cursor.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/layout/activity_register_auction.xml b/android/app/src/main/res/layout/activity_register_auction.xml new file mode 100644 index 000000000..96cbaf781 --- /dev/null +++ b/android/app/src/main/res/layout/activity_register_auction.xml @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +