diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 8d20c28..cee1912 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -82,4 +82,7 @@ dependencies {
implementation(Dependency.BottomNav.NAV_FRAGMENT)
implementation(Dependency.BottomNav.NAV_UI)
+
+ implementation("com.github.bumptech.glide:glide:4.13.0")
+ annotationProcessor("com.github.bumptech.glide:compiler:4.13.0")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c2c39ee..b78de2a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,6 +1,6 @@
+ package="com.gsps.gsp_android">
@@ -14,21 +14,20 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@style/Theme.GSP_Android" >
+ android:theme="@style/Theme.GSP_Android">
-
+ android:exported="false" />
+ android:exported="false" />
-
+
@@ -48,7 +47,7 @@
android:theme="@style/Theme.MaterialComponents.DayNight.DarkActionBar" />
+ android:exported="true">
@@ -57,7 +56,6 @@
+ android:exported="false" />
-
\ No newline at end of file
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarAdapter.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarAdapter.kt
new file mode 100644
index 0000000..ee8be81
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarAdapter.kt
@@ -0,0 +1,120 @@
+package com.gsps.gsp_android.ui.main
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.gsps.gsp_android.R
+import com.gsps.gsp_android.databinding.ItemDayBinding
+
+class CalendarAdapter(val context: Context) :
+ ListAdapter(CalendarDiffCallback()) {
+ var lastCheckedPosition = -1
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ return CalendarViewHolder(
+ ItemDayBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder is CalendarViewHolder) {
+ holder.bind(getItem(position))
+ }
+ }
+
+ fun initCheck() {
+ lastCheckedPosition = -1
+ scheduleAdapter.submitList(null)
+ }
+
+ inner class CalendarViewHolder(private val binding: ItemDayBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: CalendarDayModel) {
+ binding.rbDay.text = item.dayOfMonth.toString()
+
+ setVisibility(binding, item)
+
+ binding.rbDay.setOnClickListener {
+ if (lastCheckedPosition == adapterPosition && item.isChecked) {
+ lastCheckedPosition = -1
+ item.isChecked = false
+ } else {
+ item.isChecked = true
+ if (lastCheckedPosition != -1) {
+ getItem(lastCheckedPosition).isChecked = false
+ setVisibility(binding, getItem(lastCheckedPosition))
+ notifyItemChanged(lastCheckedPosition)
+ }
+ lastCheckedPosition = adapterPosition
+ }
+ setVisibility(binding, item)
+
+ if (item.dateType == DateType.SELECTED) {
+ scheduleAdapter.submitList(item.plan)
+ } else {
+ scheduleAdapter.submitList(null)
+ }
+ }
+ }
+
+ private fun setDateType(item: CalendarDayModel) {
+ if (item.plan.isEmpty()) item.dateType = DateType.NORMAL
+ else item.dateType = DateType.PLANNED
+
+ if (item.isChecked) item.dateType = DateType.SELECTED
+
+ if (item.month == 0) item.dateType = DateType.EMPTY
+ }
+
+ private fun setVisibility(binding: ItemDayBinding, item: CalendarDayModel) {
+ setDateType(item)
+
+ when (item.dateType) {
+ DateType.EMPTY -> {
+ binding.rbDay.visibility = View.INVISIBLE
+ binding.ivPlan.visibility = View.INVISIBLE
+ }
+ DateType.NORMAL -> {
+ binding.rbDay.isChecked = false
+ binding.rbDay.visibility = View.VISIBLE
+ binding.ivPlan.visibility = View.INVISIBLE
+ }
+ DateType.PLANNED -> {
+ binding.rbDay.isChecked = false
+ binding.rbDay.visibility = View.VISIBLE
+ binding.ivPlan.visibility = View.VISIBLE
+ binding.ivPlan.setColorFilter(
+ ContextCompat.getColor(
+ context,
+ R.color.main_lighten
+ )
+ )
+ }
+ DateType.SELECTED -> {
+ binding.rbDay.isChecked = true
+ binding.rbDay.visibility = View.VISIBLE
+ binding.ivPlan.visibility = View.INVISIBLE
+ }
+ }
+ }
+ }
+}
+
+class CalendarDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: CalendarDayModel, newItem: CalendarDayModel): Boolean {
+ return oldItem === newItem
+ }
+
+ override fun areContentsTheSame(oldItem: CalendarDayModel, newItem: CalendarDayModel): Boolean {
+ return oldItem == newItem
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarDayModel.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarDayModel.kt
new file mode 100644
index 0000000..085209c
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarDayModel.kt
@@ -0,0 +1,14 @@
+package com.gsps.gsp_android.ui.main
+
+data class CalendarDayModel(
+ var dateType: DateType = DateType.EMPTY,
+ var month: Int = 0,
+ var dayOfMonth: Int = 0,
+ var plan: MutableList = mutableListOf(),
+ var isChecked: Boolean = false,
+ var dayOfWeek: Int = 0
+)
+
+enum class DateType {
+ EMPTY, NORMAL, PLANNED, SELECTED
+}
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarFragment.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarFragment.kt
new file mode 100644
index 0000000..49cf13c
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/CalendarFragment.kt
@@ -0,0 +1,134 @@
+package com.gsps.gsp_android.ui.main
+
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.gsps.gsp_android.R
+import com.gsps.gsp_android.databinding.FragmentCalendarBinding
+import com.gsps.gsp_android.ui.base.BaseFragment
+import com.gsps.gsp_android.utils.DimensionUtils
+import java.util.*
+
+lateinit var scheduleAdapter: ScheduleAdapter
+
+class CalendarFragment : BaseFragment(R.layout.fragment_calendar) {
+ private val calendarAdapter: CalendarAdapter by lazy { CalendarAdapter(requireContext()) }
+
+ override fun initView() {
+ scheduleAdapter = ScheduleAdapter(requireContext())
+
+ binding.dayContainer.apply {
+ itemAnimator = null
+ layoutManager = GridLayoutManager(context, 7, GridLayoutManager.VERTICAL, false)
+ adapter = calendarAdapter
+ }
+
+ binding.scheduleContainer.apply {
+ itemAnimator = null
+ layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
+ adapter = scheduleAdapter
+ }
+
+ var calendar = Calendar.getInstance()
+
+ calendar.timeInMillis = System.currentTimeMillis()
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+
+ val tmpCal = calendar.timeInMillis
+ calendar.timeInMillis = tmpCal
+
+ setCalendar(calendar)
+
+ binding.btnLastMonth.setOnClickListener {
+ calendar = changeCalendar(calendar, -1)
+ setCalendar(calendar)
+ }
+
+ binding.btnNextMonth.setOnClickListener {
+ calendar = changeCalendar(calendar, +1)
+ setCalendar(calendar)
+ }
+
+ binding.tvMonth.setOnClickListener {
+ calendar.timeInMillis = System.currentTimeMillis()
+ calendar.set(Calendar.DAY_OF_MONTH, 1)
+
+ val tmpCal = calendar.timeInMillis
+ calendar.timeInMillis = tmpCal
+
+ setCalendar(calendar)
+ }
+
+ binding.scheduleContainer.addItemDecoration(object : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(
+ outRect: Rect,
+ view: View,
+ parent: RecyclerView,
+ state: RecyclerView.State
+ ) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ val dimension = DimensionUtils()
+
+ when (parent.getChildAdapterPosition(view)) {
+ 0 -> {
+ outRect.top = dimension.dpToPx(context!!, 16f)
+ outRect.bottom = dimension.dpToPx(context!!, 8f)
+ }
+ scheduleAdapter.itemCount - 1 -> {
+ outRect.top = dimension.dpToPx(context!!, 8f)
+ outRect.bottom = dimension.dpToPx(context!!, 16f)
+ }
+ else -> {
+ outRect.top = dimension.dpToPx(context!!, 8f)
+ outRect.bottom = dimension.dpToPx(context!!, 8f)
+ }
+ }
+
+ if (scheduleAdapter.itemCount == 1) {
+ outRect.top = dimension.dpToPx(context!!, 16f)
+ outRect.bottom = dimension.dpToPx(context!!, 16f)
+ }
+ }
+ })
+ }
+
+ private fun changeCalendar(calendar: Calendar, factor: Int): Calendar {
+ val year = calendar.get(Calendar.YEAR)
+ val month = calendar.get(Calendar.MONTH) + 1
+
+ when (factor) {
+ -1 -> {
+ if (month == 1)
+ calendar.set(year - 1, 11, 1)
+ else
+ calendar.set(year, month - 2, 1)
+ }
+ +1 -> {
+ if (month == 12)
+ calendar.set(year + 1, 0, 1)
+ else
+ calendar.set(year, month, 1)
+ }
+ }
+ return calendar
+ }
+
+ private fun setCalendar(calendar: Calendar) {
+ val maxDate = calendar.getActualMaximum(Calendar.DATE)
+ val week = calendar.get(Calendar.DAY_OF_WEEK) - 1
+ val month = calendar.get(Calendar.MONTH) + 1
+ val list = MutableList(week, init = { CalendarDayModel() })
+
+ for (i in 1..maxDate) {
+ list.add(CalendarDayModel(DateType.NORMAL, month, i))
+ }
+
+ binding.tvMonth.text = "${calendar.get(Calendar.YEAR)}년 ${month}월"
+
+ calendarAdapter.initCheck()
+ calendarAdapter.submitList(list)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/InterestsFullListAdapter.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/InterestsFullListAdapter.kt
index 906e135..e24ec0d 100644
--- a/app/src/main/java/com/gsps/gsp_android/ui/main/InterestsFullListAdapter.kt
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/InterestsFullListAdapter.kt
@@ -6,14 +6,12 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.gsps.gsp_android.databinding.InterestsFullListItemBinding
-class InterestsFullListAdapter(
- context: Context,
- private val itemList: MutableList
-) : RecyclerView.Adapter() {
+class InterestsFullListAdapter(context: Context, private val itemList: MutableList) :
+ RecyclerView.Adapter() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
- ): InterestsFullListAdapter.InterestsFullListHolder {
+ ): InterestsFullListHolder {
val binding =
InterestsFullListItemBinding.inflate(
LayoutInflater.from(parent.context),
@@ -24,7 +22,7 @@ class InterestsFullListAdapter(
}
override fun onBindViewHolder(
- holder: InterestsFullListAdapter.InterestsFullListHolder,
+ holder: InterestsFullListHolder,
position: Int
) {
holder.bind(itemList[position])
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/MainActivity.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/MainActivity.kt
index 36f4310..bef0587 100644
--- a/app/src/main/java/com/gsps/gsp_android/ui/main/MainActivity.kt
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/MainActivity.kt
@@ -1,7 +1,7 @@
package com.gsps.gsp_android.ui.main
-import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
import com.gsps.gsp_android.R
class MainActivity : AppCompatActivity() {
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleAdapter.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleAdapter.kt
new file mode 100644
index 0000000..98a451c
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleAdapter.kt
@@ -0,0 +1,80 @@
+package com.gsps.gsp_android.ui.main
+
+import android.content.Context
+import android.graphics.drawable.GradientDrawable
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.gsps.gsp_android.R
+import com.gsps.gsp_android.databinding.ItemScheduleBinding
+import java.time.format.DateTimeFormatter
+
+class ScheduleAdapter(private val context: Context) :
+ ListAdapter(ScheduleDiffCallback()) {
+ lateinit var holder: ScheduleViewHolder
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ScheduleViewHolder {
+ val binding =
+ ItemScheduleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return ScheduleViewHolder(context, binding)
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ if (holder is ScheduleViewHolder) {
+ holder.setScheduleView(position)
+ holder.bind(getItem(position))
+ this.holder = holder
+ }
+ }
+}
+
+class ScheduleViewHolder(private val context: Context, private val binding: ItemScheduleBinding) :
+ RecyclerView.ViewHolder(binding.root) {
+ fun bind(item: ScheduleModel) {
+ with(binding) {
+ tvScheduleTitle.text = item.title
+ tvLocation.text = item.place
+ tvUserName.text = item.companyName
+ tvScheduleTime.text = item.start.format(DateTimeFormatter.ofPattern("a H:mm"))
+
+ clItem.setOnClickListener {
+ val dialog = ScheduleDialog(context)
+ dialog.start(context, item)
+ }
+ }
+ }
+
+ fun setScheduleView(position: Int) {
+ val drawable: GradientDrawable =
+ ContextCompat.getDrawable(context, R.drawable.circle) as GradientDrawable
+
+ when (position % 3) {
+ 0 -> {
+ drawable.setColor(context.getColor(R.color.main_lighten))
+ binding.icCircle.setImageDrawable(drawable)
+ }
+ 1 -> {
+ drawable.setColor(context.getColor(R.color.main))
+ binding.icCircle.setImageDrawable(drawable)
+ }
+ 2 -> {
+ drawable.setColor(context.getColor(R.color.main_darken))
+ binding.icCircle.setImageDrawable(drawable)
+ }
+ }
+ }
+}
+
+class ScheduleDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: ScheduleModel, newItem: ScheduleModel): Boolean {
+ return oldItem === newItem
+ }
+
+ override fun areContentsTheSame(oldItem: ScheduleModel, newItem: ScheduleModel): Boolean {
+ return oldItem == newItem
+ }
+}
+
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleDialog.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleDialog.kt
new file mode 100644
index 0000000..5600c30
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleDialog.kt
@@ -0,0 +1,52 @@
+package com.gsps.gsp_android.ui.main
+
+import android.app.Dialog
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.Window
+import android.view.WindowManager
+import androidx.databinding.DataBindingUtil
+import com.bumptech.glide.Glide
+import com.gsps.gsp_android.R
+import com.gsps.gsp_android.databinding.DialogScheduleBinding
+import com.gsps.gsp_android.utils.DimensionUtils
+import java.time.format.DateTimeFormatter
+
+class ScheduleDialog(context: Context) : Dialog(context) {
+ private val dialog = Dialog(context)
+ private val binding: DialogScheduleBinding =
+ DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_schedule, null, false)
+ private val defaultImage = R.drawable.rounded_corner_4dp
+
+ fun start(context: Context, item: ScheduleModel) {
+ val dimension = DimensionUtils()
+
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
+ dialog.setContentView(binding.root)
+ dialog.setCancelable(true)
+
+ with(binding) {
+ startTime.text = item.start.format(DateTimeFormatter.ofPattern("M월 d일 a H:mm"))
+ finishTime.text = item.end.format(DateTimeFormatter.ofPattern("M월 d일 a H:mm"))
+ tvScheduleTitle.text = item.title
+ place.text = "${item.place} ${item.placeDetail}"
+ userName.text = item.companyName
+
+ Glide.with(context)
+ .load(item.logoUrl)
+ .placeholder(defaultImage)
+ .error(defaultImage)
+ .fallback(defaultImage)
+ .into(ivLogo)
+
+ btnClose.setOnClickListener {
+ dialog.dismiss()
+ }
+ }
+ dialog.show()
+ dialog.window?.setLayout(
+ dimension.dpToPx(context, 320f),
+ WindowManager.LayoutParams.WRAP_CONTENT
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleModel.kt b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleModel.kt
new file mode 100644
index 0000000..d0d07d3
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/ui/main/ScheduleModel.kt
@@ -0,0 +1,17 @@
+package com.gsps.gsp_android.ui.main
+
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+
+data class ScheduleModel(
+ var start: LocalDateTime = Instant.ofEpochMilli(System.currentTimeMillis())
+ .atZone(ZoneId.systemDefault()).toLocalDateTime(),
+ var end: LocalDateTime = Instant.ofEpochMilli(System.currentTimeMillis())
+ .atZone(ZoneId.systemDefault()).toLocalDateTime(),
+ var title: String = "",
+ var place: String = "",
+ var placeDetail: String = "",
+ var companyName: String = "",
+ var logoUrl: String = ""
+)
diff --git a/app/src/main/java/com/gsps/gsp_android/utils/DimensionUtils.kt b/app/src/main/java/com/gsps/gsp_android/utils/DimensionUtils.kt
new file mode 100644
index 0000000..a87ace9
--- /dev/null
+++ b/app/src/main/java/com/gsps/gsp_android/utils/DimensionUtils.kt
@@ -0,0 +1,14 @@
+package com.gsps.gsp_android.utils
+
+import android.content.Context
+import android.util.TypedValue
+
+class DimensionUtils {
+ fun dpToPx(context: Context, dp: Float): Int {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ dp.toFloat(),
+ context.resources.displayMetrics
+ ).toInt()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/circle.xml b/app/src/main/res/drawable/circle.xml
new file mode 100644
index 0000000..9915a0b
--- /dev/null
+++ b/app/src/main/res/drawable/circle.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/item_calendar_day_background.xml b/app/src/main/res/drawable/item_calendar_day_background.xml
new file mode 100644
index 0000000..72f95a6
--- /dev/null
+++ b/app/src/main/res/drawable/item_calendar_day_background.xml
@@ -0,0 +1,15 @@
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/item_calendar_day_textcolor.xml b/app/src/main/res/drawable/item_calendar_day_textcolor.xml
new file mode 100644
index 0000000..ed232f3
--- /dev/null
+++ b/app/src/main/res/drawable/item_calendar_day_textcolor.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index f0c5698..34223ea 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,6 +1,5 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_calendar.xml b/app/src/main/res/layout/fragment_calendar.xml
new file mode 100644
index 0000000..2ff4097
--- /dev/null
+++ b/app/src/main/res/layout/fragment_calendar.xml
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_day.xml b/app/src/main/res/layout/item_day.xml
new file mode 100644
index 0000000..71813b8
--- /dev/null
+++ b/app/src/main/res/layout/item_day.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_schedule.xml b/app/src/main/res/layout/item_schedule.xml
new file mode 100644
index 0000000..c6c3d00
--- /dev/null
+++ b/app/src/main/res/layout/item_schedule.xml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 598c0b5..2a5e272 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -44,4 +44,6 @@
최근 검색어
협업 신청하러가기
신규가입회사
+ 일정
+ 상세보기
\ No newline at end of file