ํด๋น ํ๋ก์ ํธ GitHub Repository
https://github.com/ssarisong/Green-Market
GitHub - ssarisong/Green-Market: 2023๋ ํ์ฑ๋ํ๊ต ๊ณ ๊ธ๋ชจ๋ฐ์ผํ๋ก๊ทธ๋๋ฐ ํ ํ๋ก์ ํธ
2023๋ ํ์ฑ๋ํ๊ต ๊ณ ๊ธ๋ชจ๋ฐ์ผํ๋ก๊ทธ๋๋ฐ ํ ํ๋ก์ ํธ. Contribute to ssarisong/Green-Market development by creating an account on GitHub.
github.com
1. ์ฃผ์ ์ค๋ช
ํ์ฑ๋ํ๊ต ๊ณ ๊ธ๋ชจ๋ฐ์ผํ๋ก๊ทธ๋๋ฐ ๊ณผ๋ชฉ ๊ธฐ๋ง ํ๋ก์ ํธ์ ๋๋ค.
๊ณ ๊ธ๋ชจ๋ฐ์ผํ๋ก๊ทธ๋๋ฐ ์์ ์์ ๋ฐฐ์ด ๊ธฐ๋ฅ๋ค์ ์ต๋ํ ํ์ฉํ๊ธฐ ์ํด์ ์ฌ๋ฌ๊ฐ์ง ๊ธฐ๋ฅ์ ์ด์งํฉ์ด๋ผ๊ณ ์๊ฐํ๋ ์ค๊ณ ๋ง์ผ์ ์ฃผ์ ๋ก ์ ์ ํ๊ฒ ๋์์ต๋๋ค.
2. ๊ฐ๋ฐ ๊ธฐ๊ฐ
2023.08.24 ~ 2023.12.02 (์ฝ 3๊ฐ์)
3. ์ญํ
ํ์ฅ, ๋ฐฑ์๋ ์ด๊ด
- ์ค๊ณ
- FireStore ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ
- ๋ก๊ทธ์ธ ๊ณผ์ ์ค๊ณ
- Storage์ ์ ์ฌ๋ Resource ๊ตฌ์กฐ ์ค๊ณ
- ๊ตฌํ
- ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ๊ตฌํ
- ์ค๊ณ ๋ง์ผ ๊ฒ์๊ธ CRUD ๊ตฌํ
- ์ฑํ ๊ธฐ๋ฅ ๊ตฌํ
- ์ฐ๊ฒฐ
- ๊ฐ๋ฐ๋ Util ๊ธฐ๋ฅ UI์ ์ฐ๊ฒฐ
4. ๊ธฐ์ ์คํ
- ์ธ์ด: Kotlin
- ํ๋ ์์ํฌ: Android
- BaaS: Firebase
- ๋ฐ์ดํฐ๋ฒ ์ด์ค: Firestore
- ๋ฌธ์ํ: Dokka
5. ์ ์ฒด ๊ตฌ์กฐ
6. ์์ธ ๊ตฌํ ๋ด์ฉ
BaaS ์๋น์ค๋ฅผ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ฐฑ์๋ ๊ธฐ๋ฅ์ ๋ฐ๋ก Api๋ก ๊ตฌํํ์ง ์์๊ณ , Util ํด๋์ค๋ฅผ ๋ง๋ค์ด UI์์ ์ฌ์ฉํ ๊ธฐ๋ฅ์ ํจ์๋ก ํํํ์ต๋๋ค.
Callback ํจ์๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ ์ด์
Firebase์์ ์์ ์ ์ฒ๋ฆฌํ๊ณ ๋ค์ ์ ํ๋ฆฌ์ผ์ด์ ์ผ๋ก ๋์์ค๋ ๊ณผ์ ์์ ๋น๋๊ธฐ ์์ ์ด ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ Callback ํจ์๋ก ๊ตฌํํ์ต๋๋ค.
6-1. ๋ก๊ทธ์ธ
Firebase์์ Authentication ๊ธฐ๋ฅ์ ํ์ฑํํ์ต๋๋ค.
๋ก๊ทธ์ธ ๋ฐฉ๋ฒ์ผ๋ก๋ ์ด๋ฉ์ผ / ๋น๋ฐ๋ฒํธ ๋ฐฉ๋ฒ์ ์ฑํํ๊ณ , Firestore์๋ ํ์์ ๋ณด๋ฅผ ์ ์ฅํ์ต๋๋ค.
Firestore์ ํ์์ ๋ณด๋ฅผ ์ ์ฅํ ์ด์
Authentication์๋ ์ฌ์ฉ์์ ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ก๋ง ์ ์ฅํ๊ธฐ ๋๋ฌธ์ ์ถ๊ฐ์ ์ธ ์ฌ์ฉ์ ๊ด๋ จ ์ ๋ณด๋ฅผ ์ ์ฅํ ์ ์์์ต๋๋ค.
์ด ๋๋ฌธ์ Firestore ๊ธฐ๋ฅ์ ํ์ฑํํ์๊ณ , ์ฌ์ฉ์ ์ ๋ณด์ธ ์๋ ์์ผ ๋ฑ์ ์ ๋ณด๋ฅผ ์ ์ฅํ๊ณ ๊ฐ์ ธ์ฌ ์ ์๋๋ก ๊ฐ์ ํ์ต๋๋ค.
[ํ์๊ฐ์ ๊ตฌํ]
fun doSignUp(userEmail: String, password: String, name: String, birth: String, callback: (Int, String?) -> Unit){
Firebase.auth.createUserWithEmailAndPassword(userEmail, password)
.addOnCompleteListener{additionTask ->
if(additionTask.isSuccessful){
val uid = Firebase.auth.currentUser?.uid.toString()
val currentUser = Firebase.auth.currentUser
val parser = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val birthDate = Timestamp(parser.parse(birth))
val newUser = User(userEmail, name, birthDate, Timestamp.now())
userModel.insertUser(uid, newUser) { STATUS_CODE ->
if (STATUS_CODE == StatusCode.SUCCESS) {
callback(StatusCode.SUCCESS, uid)
} else {
currentUser?.delete()
?.addOnCompleteListener { deletionTask ->
if (deletionTask.isSuccessful) {
Log.d("FirebaseLoginUtils", "[${uid}] ๊ณ์ DB ์ ๋ณด ์ถ๊ฐ ์คํจ๋ก ์ธํ Auth ๊ณ์ ์ญ์ ์ฑ๊ณต")
} else {
Log.w("FirebaseLoginUtils", "[${uid}] ๊ณ์ DB ์ ๋ณด ์ถ๊ฐ ์คํจ๋ก ์ธํ Auth ๊ณ์ ์ญ์ ์คํจ!!! -> ", deletionTask.exception)
}
}
callback(StatusCode.FAILURE, null)
}
}
}else{
Log.w("FirebaseLoginUtils", "Auth์์ ์ฌ์ฉ์ ์์ฑ ์ค ์๋ฌ ๋ฐ์!!! -> ", additionTask.exception)
callback(StatusCode.FAILURE, null)
}
}
}
[ํ์์ ๋ณด Firebase์ ์ ์ฅ]
fun insertUser(uid: String, user: User, callback: (Int) -> Unit) {
db.collection("User").document(uid).set(user)
.addOnSuccessListener {
Log.d("FirestoreUserModel", "[${uid}]:์ฌ์ฉ์๋ช
[${user.name}]:์ฌ์ฉ์์ด๋ฉ์ผ[(${user.email})] ์ฌ์ฉ์ DB ์ ๋ณด ์ฑ๊ณต์ ์ผ๋ก ์์ฑ")
callback(StatusCode.SUCCESS)
}
.addOnFailureListener { e ->
Log.w("FirestoreUserModel", "[${uid}] ์ฌ์ฉ์ DB ์ ๋ณด ์์ฑ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE)
}
}
6-2. ์ค๊ณ ๋ง์ผ ๊ฒ์๊ธ
์ค๊ณ ๋ง์ผ ๊ฒ์๊ธ์ Firestore๋ฅผ ํตํด CRUD ๊ธฐ๋ฅ์ ๊ตฌํํ์ต๋๋ค.
์ค๊ณ ๋ฌผํ ์ด๋ฏธ์ง๊ฐ ์ฒจ๋ถ ๊ฐ๋ฅํด์ผ ํ๊ธฐ ๋๋ฌธ์, Firebase์ Storage ๊ธฐ๋ฅ์ ํ์ฑํํ์ต๋๋ค.
์ด๋ฏธ์ง ์ ๋ก๋ ๊ด๋ จ ์์ธ์ฒ๋ฆฌ ๋ฐฉ์
์ด๋ฏธ์ง๋ฅผ ์ ๋ก๋ ํ๊ฑฐ๋ ๊ฐ์ ธ์ค๋ ๊ณผ์ ์์ ์ฌ๋ฌ ์ด์ ๋ก ์์ธ ์ฌํญ์ด ๋ฐ์ํ ์ผ์ด ๋ง์์ต๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด addOnFailureListener{}๋ฅผ ํ์ฉํ์ฌ ์์ธ ์ฌํญ์ ๋ก๊ทธ๋ก ์ ๋ฌํ๊ณ , StatusCode๋ฅผ ๋ง๋ค์ด์ http์ ์ํ์ฝ๋์ฒ๋ผ ์๋ํ๋๋ก ํ์ฌ Callback์ผ๋ก return ํ์ต๋๋ค.
์ด ๋ฐฉ์์ ์ฌ์ฉํ์ฌ ๊ธฐ๋ฅ ์ค ์์ธ ์ฌํญ์ด ๋ฐ์ํ์ ๋, ์ด๋ค ์ด์ ์ ์๋ฌ์ธ์ง ํ์ ํ๊ธฐ ์ฌ์ ์ต๋๋ค.
[์ด๋ฏธ์ง ์ ๋ก๋ ๊ณผ์ - ์ค๊ณ ๋ฌผํ ์ถ๊ฐ ๊ณผ์ ์ค]
...
imageRef.putFile(Uri.parse(product.img))
.addOnSuccessListener {
imageRef.downloadUrl.addOnSuccessListener { uri ->
val updatedProduct =
product.copy(productId = newProduct.id, img = uri.toString())
newProduct.set(updatedProduct).addOnSuccessListener {
Log.d(
"FirestoreProductModel",
"([${newProduct.id}]:์ํ๋ช
[${product.name}] ์ํ DB ์ฑ๊ณต์ ์ผ๋ก ์์ฑ"
)
callback(StatusCode.SUCCESS, newProduct.id)
}.addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${newProduct.id}]:์ํ๋ช
[${product.name}] ์ํ DB ์ ๋ณด ์์ฑ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE, null)
}
}.addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${newProduct.id}]:์ํ๋ช
[${product.name}] ์ํ ์ด๋ฏธ์ง ๋ค์ด๋ก๋ URL ๋ฐ๊ธฐ ์คํจ!!! -> ", e)
callback(StatusCode.FAILURE, null)
}
}
.addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${newProduct.id}]:์ํ๋ช
[${product.name}]์ํ ์ด๋ฏธ์ง ์ ์ฅ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE, null)
}
...
[ ์ด๋ฏธ์ง ์ญ์ ๊ณผ์ - ์ค๊ณ ๋ฌผํ ์ญ์ ๊ณผ์ ์ค ]
...
val imageRef = FirebaseStorage.getInstance().reference.child("Product/${pid}")
imageRef.metadata
.addOnSuccessListener {
imageRef.delete()
.addOnSuccessListener {
db.collection("Product").document(pid).delete()
.addOnSuccessListener {
Log.d("FirestoreProductModel", "[${pid}] ์ํ DB ์ ๋ณด ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ")
callback(StatusCode.SUCCESS)
}
.addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${pid}] ์ํ DB ์ ๋ณด ์ญ์ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE)
}
} .addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${pid}] ์ํ ์ด๋ฏธ์ง Storage์์ ์ญ์ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE)
}
}
.addOnFailureListener {
db.collection("Product").document(pid).delete()
.addOnSuccessListener {
Log.d("FirestoreProductModel", "[${pid}] ์ํ DB ์ ๋ณด ์ฑ๊ณต์ ์ผ๋ก ์ญ์ ")
callback(StatusCode.SUCCESS)
}
.addOnFailureListener { e ->
Log.w("FirestoreProductModel", "[${pid}] ์ํ DB ์ ๋ณด ์ญ์ ์ค ์๋ฌ ๋ฐ์!!! -> ", e)
callback(StatusCode.FAILURE)
}
}
...
6-3. ์ฑํ
์ฑํ ์ Firestore Database์ ์ํด ๊ด๋ฆฌ๋๋๋ก ํ์ต๋๋ค.
NoSQL์ธ Firestore์ ํน์ฑ์ ํ์ฉํ์ฌ ์๋์ ๊ฐ์ด ChatRoom์ด๋ ์ปฌ๋ ์ ์ ๋ง๋ค๊ณ ๊ทธ ์์์ ์์ด๋ ๋ชจ๋ ์ฑํ ๋ฉ์์ง๋ค์ Chat์ด๋ ํ์ ์ปฌ๋ ์ ์ผ๋ก ๊ตฌ์ฑํ์ต๋๋ค.
[์ฑํ DB ๊ตฌ์ฑ ์์]
ChatRoom [
{
chatRoomId : "3pjzurfyPFkyebOVX6mf"
productId : "zmaQgjjQLZ5W05ZAjwXf"
buyerId : "vwyi7dnkKCaH5adeXPk1NrsEdue2"
sellerId : "kcjEOOK3VtOk0rTpnks8QTPxLbJ2"
lastMessage : "์์ง ์์ต๋๋ค!"
lastMessageAt : 2023๋
12์ 1์ผ ์คํ 7์ 33๋ถ 59์ด UTC+9
createdAt : 2023๋
12์ 1์ผ ์คํ 7์ 33๋ถ 36์ด UTC+9
Chat : [
{
senderId : "vwyi7dnkKCaH5adeXPk1NrsEdue2"
message : "ํ๋ ธ์๊น์?"
createdAt : 2023๋
12์ 1์ผ ์คํ 7์ 33๋ถ 36์ด UTC+9
},
{
senderId : "kcjEOOK3VtOk0rTpnks8QTPxLbJ2"
message : "์์ง ์์ต๋๋ค!"
createdAt : 2023๋
12์ 1์ผ ์คํ 7์ 33๋ถ 59์ด UTC+9
},
...
]
},
...
]
์ฑํ ์์ ๋ฉ์์ง๋ ์๋๋ฐฉ์ด ๋ณด๋ด๋ ์ฆ์ ํ์ธ์ด ๊ฐ๋ฅํด์ผ ํ์ต๋๋ค.
์ด๋ฅผ ์ํด Listener ๋ฉ์๋๋ฅผ ๊ตฌํํ์๊ณ , ์ด๋ฅผ ํตํด ๋ฉ์์ง๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ์ฌ ๋ ์ฌ์ฉ์๊ฐ ๋ฐ๋ก๋ฐ๋ก ๋ฉ์์ง๋ฅผ ์ฃผ๊ณ ๋ฐ์ ์ ์๋๋ก ํ์ต๋๋ค.
๋ํ, addSnapshotListener{} ๊ธฐ๋ฅ์ ํ์ฉํ์๋๋ฐ, ์ด๋ query๊ฐ ๊ฐ์งํ๋ ๋ฐ์ดํฐ๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์ ธ์ค๊ณ ๋ณ๊ฒฝ์ด ๋ฐ์ํ ๋๋ง๋ค ์ฝ๋ฐฑ์ ์คํํฉ๋๋ค.
[์ฑํ ๋ฐฉ ๋ด์์ ๋ฉ์ง์ง ๊ฐ์ง Listener]
/**
* ํน์ ์ฑํ
๋ฐฉ์ ๋ฉ์์ง๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํฉ๋๋ค.
*
* @param chatRoomId ๊ฐ์งํ ์ฑํ
๋ฐฉ์ ID.
* @param callback ์ ๋ฉ์์ง ๋๋ ์ค๋ฅ ๋ฐ์ ์ ์ํ ์ฝ๋(StatusCode)์ ๋ฉ์์ง ๋ด์ฉ์ ์ธ์๋ก ๋ฐ๋ ์ฝ๋ฐฑ ํจ์์
๋๋ค.
* @return ๋ฆฌ์ค๋ ๋ฑ๋ก์ ํด์ ํ๋ ํจ์.
*/
fun listenForMessages(chatRoomId: String, callback: (Int, List<Chat>?) -> Unit): () -> Unit {
val chatMessagesRef = db.collection("ChatRoom").document(chatRoomId).collection("Chat")
val query = chatMessagesRef.orderBy("createdAt", Query.Direction.ASCENDING)
val registration = query.addSnapshotListener { snapshots, e ->
if (e != null) {
Log.w("FirestoreChattingModel", "[$chatRoomId] ๋ฉ์์ง ์ค์๊ฐ ๊ฐ์ง ์คํจ!!! -> ", e)
callback(StatusCode.FAILURE, null)
return@addSnapshotListener
}
if (snapshots != null && !snapshots.isEmpty) {
val messages = snapshots.mapNotNull { it.toObject(Chat::class.java) }
Log.d("firestoreChattingModel", "[$chatRoomId] ์ฑํ
๋ฐฉ์ ์ฑํ
๋ชฉ๋ก ๋ถ๋ฌ์ค๊ธฐ ์๋ฃ")
callback(StatusCode.SUCCESS, messages)
} else {
Log.d("FirestoreChattingModel", "[$chatRoomId] ๋ฉ์์ง๊ฐ ์์ง ์์")
callback(StatusCode.SUCCESS, emptyList())
}
}
return { registration.remove() }
}
๋ํ, ์ฑํ ๋ฐฉ ๋ฐ์ ์ฌ์ฉ์๊ฐ ์์นํ ๊ฒฝ์ฐ์ ์๋ก์ด ์ฑํ ์ด ๋ฐ์ํ๋ฉด ๋ง์ง๋ง ์ฑํ ๋ด์ฉ์ ํ์ธํ ์ ์์ด์ผ ํ๊ธฐ ๋๋ฌธ์ ๋ง์ง๋ง ์ฑํ ๋ฉ์์ง๋ฅผ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ๋ Listener๋ ๊ตฌํํ์ต๋๋ค.
[์ฑํ ๋ฐฉ ์ธ์์ ๋ฉ์์ง ๊ฐ์ง Listener]
/**
* Firestore์ ํน์ ์ฑํ
๋ฐฉ์ ๋ํ lastMessage ๋ณ๊ฒฝ์ ์ค์๊ฐ์ผ๋ก ๊ฐ์งํฉ๋๋ค.
*
* @param chatRoomId ์ค์๊ฐ์ผ๋ก ๊ฐ์งํ ์ฑํ
๋ฐฉ์ ID์
๋๋ค.
* @param callback lastMessage ์ ๋ณด๋ฅผ ๋ฐํํ๋ ์ฝ๋ฐฑ ํจ์์
๋๋ค.
*/
fun listenForLastMessage(chatRoomId: String, callback: (Int, String?, Timestamp?) -> Unit) {
db.collection("ChatRoom").document(chatRoomId)
.addSnapshotListener { snapshot, e ->
if (e != null) {
Log.w("FirestoreChatModel", "[${chatRoomId}]์ฑํ
๋ฐฉ lastMessage ๋ฆฌ์ค๋ ์ค๋ฅ ๋ฐ์!!! -> ", e)
return@addSnapshotListener
}
if (snapshot != null && snapshot.exists()) {
Log.d("FirestoreChatModel", "[${chatRoomId}] ์ฑํ
๋ฐฉ์ ๋ง์ง๋ง ๋ฉ์์ง์ ๋ณด๋ธ ์๊ฐ ์ค์๊ฐ ๋ถ๋ฌ์ค๊ธฐ ์๋ฃ")
callback(StatusCode.SUCCESS, snapshot.getString("lastMessage"), snapshot.getTimestamp("lastMessageAt"))
} else {
Log.d("FirestoreChatModel", "[${chatRoomId}] ์ฑํ
๋ฐฉ์ lastMessage ๋ฐ์ดํฐ ์์")
callback(StatusCode.FAILURE, null, null)
}
}
}
7. ์คํ ์๋๋ฆฌ์ค
7-1. ํ์๊ฐ์ , ๋ก๊ทธ์ธ
- ํ์๊ฐ์ : ์ด๋ฉ์ผ, ๋น๋ฐ๋ฒํธ, ์ด๋ฆ, ์๋ ์์ผ ๋ชจ๋ ์ ๋ ฅํด์ผํฉ๋๋ค.
- ๋ก๊ทธ์ธ: ํ์๊ฐ์ ํ, ์ด๋ฉ์ผ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ ๋ ฅํด์ผํฉ๋๋ค.
7-2. ํ ํ๋ฉด
- ๋ฑ๋ก๋์ด ์๋ ์ค๊ณ ๋ฌผํ ๊ฒ์๊ธ์ ํ์ธํ ์ ์์ต๋๋ค.
7-3. ์ค๊ณ ๋ฌผํ ๊ฒ์ ํํฐ
- ํ๋งค์ค, ์์ฝ์ค, ํ๋งค ์๋ฃ๋ณ๋ก ๋ฑ๋ก๋์ด ์๋ ์ค๊ณ ๋ฌผํ ๊ฒ์๊ธ์ ํ์ธํ ์ ์์ต๋๋ค.
7-4. ์ค๊ณ ๋ฌผํ ์์ธ ํ์ด์ง
- ๊ฐ๊ฐ์ ์ค๊ณ ๋ฌผํ๋ง๋ค ์ํ ํํฉ์ ๋ณผ ์ ์๊ณ , '์ฑํ ํ๊ธฐ' ๋ฒํผ์ ํตํด ํ๋งค์์ ์ฑํ ์ด ๊ฐ๋ฅํฉ๋๋ค.
7-5. ์ค๊ณ ๋ฌผํ ๊ฒ์๊ธ ๋ฑ๋ก
- '์ํ๋ฑ๋ก' ๋ฒํผ์ ๋๋ฅด๋ฉด ์ค๊ณ ๋ฌผํ ๊ฒ์๊ธ์ ๋ฑ๋กํ ์ ์๋ ํ๋ฉด์ด ๋์ต๋๋ค.
- ์ ๋ชฉ, ๊ฐ๊ฒฉ, ์ํ ์ค๋ช ์ ์ฐ๊ณ ์ฌ์ง์ ์ฒจ๋ถํ ํ์ ๊ฒ์๊ธ์ ์ฌ๋ฆด ์ ์์ต๋๋ค.
7-6. ์ค๊ณ ๋ฌผํ ๊ฒ์๊ธ ์์
- ์ค๊ณ ๋ฌผํ ์ ๋ณด์ ํ๋งค ์ํ ์์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
7-7. ์ฑํ
7-8. ๋ง์ดํ์ด์ง
- '๋ด๊ฐ ์ด ๊ธ'์์ ์ฌ์ฉ์๊ฐ ์์ฑํ ๊ธ์ ํ์ธํ ์ ์์ผ๋ฉฐ, ํด๋น ํ์ด์ง์์ ๊ฒ์๊ธ ์์ ์ด ๊ฐ๋ฅํฉ๋๋ค.
- ๋ก๊ทธ์์ ํ๋ฉด ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํฉ๋๋ค.
'PROJECT๐ป' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
[iOS] ํ์ฑ๋ ๊ธฐ์์ฌ ์ดํ ํ๋ก์ ํธ (0) | 2025.02.13 |
---|---|
[Web] ์์ทจ๋ง๋ ํ๋ก์ ํธ (0) | 2025.02.11 |