Skip to content

Commit

Permalink
Merge pull request #207 from ShanePark/#156
Browse files Browse the repository at this point in the history
#156 schedule search
  • Loading branch information
ShanePark authored Jan 14, 2025
2 parents 0006beb + c71427c commit 57674c9
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,17 @@ class FriendService(
}
}

@Transactional(readOnly = true)
fun availableVisibilities(loginMember: LoginMember?, member: Member): Set<Visibility> {
if (loginMember == null)
return setOf(Visibility.PUBLIC)
if (loginMember.id == member.id) {
return Visibility.entries.toSet()
}
if (isFriend(loginMember.id, member.id!!)) {
return setOf(Visibility.PUBLIC, Visibility.FRIENDS)
}
return setOf(Visibility.PUBLIC)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ package com.tistory.shanepark.dutypark.schedule.controller

import com.tistory.shanepark.dutypark.member.domain.annotation.Login
import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleDto
import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleSearchResult
import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleUpdateDto
import com.tistory.shanepark.dutypark.schedule.service.ScheduleSearchService
import com.tistory.shanepark.dutypark.schedule.service.ScheduleService
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.web.PageableDefault
import org.springframework.http.ResponseEntity
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
Expand All @@ -15,6 +20,7 @@ import java.util.*
@RequestMapping("/api/schedules")
class ScheduleController(
private val scheduleService: ScheduleService,
private val scheduleSearchService: ScheduleSearchService,
) {

@GetMapping
Expand All @@ -31,6 +37,16 @@ class ScheduleController(
)
}

@GetMapping("/{id}/search")
fun searchSchedule(
@Login(required = false) loginMember: LoginMember,
@PathVariable(value = "id") targetMemberId: Long,
@PageableDefault(size = 10) pageable: Pageable,
@RequestParam q: String
): Page<ScheduleSearchResult> {
return scheduleSearchService.search(loginMember, targetMemberId, pageable, q)
}

@PostMapping
fun createSchedule(
@RequestBody @Validated scheduleUpdateDto: ScheduleUpdateDto,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.tistory.shanepark.dutypark.schedule.domain.dto

import com.tistory.shanepark.dutypark.schedule.domain.entity.Schedule
import java.time.LocalDateTime

data class ScheduleSearchResult(
val content: String,
val startDateTime: LocalDateTime,
val endDateTime: LocalDateTime,
val visibility: String,
val isTagged: Boolean,
) {
companion object {
fun of(schedule: Schedule): ScheduleSearchResult {
return ScheduleSearchResult(
content = schedule.content,
startDateTime = schedule.startDateTime,
endDateTime = schedule.endDateTime,
visibility = schedule.visibility.name,
isTagged = schedule.tags.isNotEmpty()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,33 @@ package com.tistory.shanepark.dutypark.schedule.repository
import com.tistory.shanepark.dutypark.member.domain.entity.Member
import com.tistory.shanepark.dutypark.member.domain.enums.Visibility
import com.tistory.shanepark.dutypark.schedule.domain.entity.Schedule
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query
import java.time.LocalDateTime
import java.util.*

interface ScheduleRepository : JpaRepository<Schedule, UUID> {

@Query(
"SELECT s" +
" FROM Schedule s" +
" JOIN FETCH s.member m" +
" LEFT JOIN FETCH s.tags t" +
" LEFT JOIN FETCH t.member tm" +
" WHERE (m = :member or tm = :member)" +
" AND s.content LIKE %:content%" +
" AND s.visibility IN (:visibility)" +
" ORDER BY s.startDateTime DESC"
)
fun findByMemberAndContentContainingAndVisibilityIn(
member: Member,
content: String,
visibility: Collection<Visibility>,
pageable: Pageable,
): Page<Schedule>

@Query(
"SELECT s" +
" FROM Schedule s" +
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tistory.shanepark.dutypark.schedule.service

import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleSearchResult
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable

interface ScheduleSearchService {

fun search(loginMember: LoginMember, targetMemberId: Long, page: Pageable, query: String): Page<ScheduleSearchResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.tistory.shanepark.dutypark.schedule.service

import com.tistory.shanepark.dutypark.member.repository.MemberRepository
import com.tistory.shanepark.dutypark.member.service.FriendService
import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleSearchResult
import com.tistory.shanepark.dutypark.schedule.repository.ScheduleRepository
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service

/**
* When search engine is implemented, this service will be replaced with ScheduleSearchServiceESImpl
*/
@Service
class ScheduleSearchServiceDBImpl(
private val scheduleRepository: ScheduleRepository,
private val memberRepository: MemberRepository,
private val friendService: FriendService,
) : ScheduleSearchService {

override fun search(
loginMember: LoginMember,
targetMemberId: Long,
page: Pageable,
query: String
): Page<ScheduleSearchResult> {
val target = memberRepository.findById(targetMemberId).orElseThrow()
val availableVisibilities = friendService.availableVisibilities(loginMember, target)

return scheduleRepository.findByMemberAndContentContainingAndVisibilityIn(
member = target,
content = query,
visibility = availableVisibilities,
pageable = page
).map { ScheduleSearchResult.of(it) }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.tistory.shanepark.dutypark.schedule.service
import com.tistory.shanepark.dutypark.common.domain.dto.CalendarView
import com.tistory.shanepark.dutypark.common.exceptions.DutyparkAuthException
import com.tistory.shanepark.dutypark.member.domain.entity.Member
import com.tistory.shanepark.dutypark.member.domain.enums.Visibility
import com.tistory.shanepark.dutypark.member.repository.MemberRepository
import com.tistory.shanepark.dutypark.member.service.FriendService
import com.tistory.shanepark.dutypark.schedule.domain.dto.ScheduleDto
Expand Down Expand Up @@ -44,7 +43,7 @@ class ScheduleService(
val start = calendarView.rangeFrom
val end = calendarView.rangeEnd

val availableVisibilities = availableVisibilities(loginMember, member)
val availableVisibilities = friendService.availableVisibilities(loginMember, member)

val userSchedules =
scheduleRepository.findSchedulesOfMonth(member, start, end, visibilities = availableVisibilities)
Expand Down Expand Up @@ -75,17 +74,6 @@ class ScheduleService(
return array
}

private fun availableVisibilities(loginMember: LoginMember?, member: Member): Collection<Visibility> {
if (loginMember == null)
return setOf(Visibility.PUBLIC)
if (loginMember.id == member.id) {
return Visibility.values().toList()
}
if (friendService.isFriend(loginMember.id, member.id!!)) {
return setOf(Visibility.PUBLIC, Visibility.FRIENDS)
}
return setOf(Visibility.PUBLIC)
}

fun createSchedule(loginMember: LoginMember, scheduleUpdateDto: ScheduleUpdateDto): Schedule {
val scheduleMember = memberRepository.findById(scheduleUpdateDto.memberId).orElseThrow()
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/static/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -710,3 +710,11 @@ input[readonly], textarea[readonly] {
background-color: #f8f9fa;
cursor: not-allowed;
}

.search-bar input {
border-radius: 10px 0 0 10px;
}

.search-bar .btn {
border-radius: 0 10px 10px 0;
}
96 changes: 84 additions & 12 deletions src/main/resources/templates/duty/duty.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@
<div th:if="${member.department}" class="calendar" th:attr="data-member_id=${member.id}">
<div class="month-control row">
<div class="month-control-left col-md-2">
<div class="info department-info">
<div class="info member-info">
<i class="bi bi-people-fill"></i>
<span th:text="${member.department}"></span>
<span th:text="${member.name}"></span>
</div>
</div>
<div class="col-md-8">
Expand All @@ -36,16 +36,22 @@ <h1>
<a class="move h_pointer" v-on:click="addMonth(-1)">
<i class="bi bi-arrow-left-circle"></i>
</a>
<input id="month-selector" type="month" v-model="combinedYearMonth">
<label for="month-selector"></label><input id="month-selector" type="month" v-model="combinedYearMonth">
<a class="move h_pointer" v-on:click="addMonth(1)">
<i class="bi bi-arrow-right-circle"></i>
</a>
</h1>
</div>
<div class="month-control-right col-md-2">
<div class="info member-info">
<span th:text="${member.name}"></span>
<i class="bi bi-person-badge"></i>
<div class="month-control-right col-md-2 p-0">
<div class="search-bar d-flex align-items-center">
<form @submit.prevent="search()">
<label class="d-flex w-100">
<input type="text" class="form-control" placeholder="검색" style="flex: 1;" v-model="searchQuery">
<button class="btn btn-outline-dark" type="submit">
<i class="bi bi-search"></i>
</button>
</label>
</form>
</div>
</div>
</div>
Expand Down Expand Up @@ -248,6 +254,54 @@ <h5 class="modal-title" id="addModalLabel">할일 추가</h5>
</div>
</div>

<div id="search-result-modal" class="modal fade" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">[ <b v-text="searchQuery"></b> ] 검색 결과</h5>
<button type="button" data-bs-dismiss="modal" aria-label="Close" class="btn-close"></button>
</div>
<div class="modal-body">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">내용</th>
<th scope="col">시작일</th>
<th scope="col">종료일</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in searchResults.content" :key="index">
<th scope="row">{{ index + 1 + (searchResults.pageable.pageNumber * searchResults.pageable.pageSize) }}
</th>
<td>{{ item.content }}</td>
<td>{{ formattedDateYMD(item.startDateTime) }}</td>
<td>{{ formattedDateYMD(item.endDateTime) }}</td>
</tr>
</tbody>
</table>
<nav v-if="searchResults.totalPages > 1">
<ul class="pagination justify-content-center">
<li class="page-item" :class="{ disabled: searchResults.first }">
<button class="page-link" @click="search(searchResults.pageable.pageNumber - 1)">이전</button>
</li>
<li class="page-item"
v-for="page in searchResults.totalPages"
:key="page"
:class="{ active: page - 1 === searchResults.pageable.pageNumber }">
<button class="page-link" @click="search(page - 1)">{{ page }}</button>
</li>
<li class="page-item" :class="{ disabled: searchResults.last }">
<button class="page-link" @click="search(searchResults.pageable.pageNumber + 1)">다음</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>

<div id="detail-view-modal" class="modal fade" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content" v-if="detailView">
Expand Down Expand Up @@ -437,6 +491,8 @@ <h5 class="modal-title" id="addModalLabel">할일 추가</h5>
editTodoMode: false,
dDaysLoaded: false,
dDaysPromise: null,
searchQuery: '',
searchResults: [],
}, mounted() {
if (departmentId) {
this.loadDepartment();
Expand Down Expand Up @@ -761,14 +817,15 @@ <h5 class="modal-title" id="addModalLabel">할일 추가</h5>
minute = ':' + dateTime.getMinutes().toString().padStart(2, '0');
}
return hour + minute + (dateTime.getHours() >= 12 ? 'PM ' : 'AM');
}
,
},
formattedDateYMD(dateTimeString) {
return new Date(dateTimeString).toISOString().split('T')[0];
},
formattedDate(year, month, day) {
month = (month < 10) ? '0' + month : month;
day = (day < 10) ? '0' + day : day;
return year + '-' + month + '-' + day;
}
,
},
isToday(duty) {
const today = new Date();
return duty.year === today.getFullYear() && duty.month === today.getMonth() + 1 && duty.day === today.getDate();
Expand Down Expand Up @@ -1377,7 +1434,22 @@ <h5 class="modal-title" id="addModalLabel">할일 추가</h5>
}
app.loadDuties(app.year, app.month);
});
}
},
search(page = 0) {
$('#search-result-modal').modal('show');
const query = app.searchQuery;
fetch(`/api/schedules/${memberId}/search?q=${query}&page=${page}`)
.then((response) => {
if (!response.ok) {
console.log(`Error searching schedules. Status Code: ${response.status}`);
return;
}
return response.json();
})
.then((data) => {
app.searchResults = data;
});
},
}
}
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import com.tistory.shanepark.dutypark.department.repository.DepartmentRepository
import com.tistory.shanepark.dutypark.duty.domain.entity.DutyType
import com.tistory.shanepark.dutypark.duty.repository.DutyTypeRepository
import com.tistory.shanepark.dutypark.member.domain.dto.MemberCreateDto
import com.tistory.shanepark.dutypark.member.domain.entity.FriendRelation
import com.tistory.shanepark.dutypark.member.domain.entity.Member
import com.tistory.shanepark.dutypark.member.domain.enums.Visibility
import com.tistory.shanepark.dutypark.member.repository.FriendRelationRepository
import com.tistory.shanepark.dutypark.member.repository.MemberRepository
import com.tistory.shanepark.dutypark.member.service.MemberService
import com.tistory.shanepark.dutypark.security.domain.dto.LoginMember
Expand All @@ -36,6 +38,9 @@ class DutyparkIntegrationTest {
@Autowired
lateinit var dutyTypeRepository: DutyTypeRepository

@Autowired
lateinit var friendRelationRepository: FriendRelationRepository

@Autowired
lateinit var em: EntityManager

Expand Down Expand Up @@ -128,4 +133,9 @@ class DutyparkIntegrationTest {
memberRepository.save(target)
}

protected fun makeThemFriend(member1: Member, member2: Member) {
friendRelationRepository.save(FriendRelation(member1, member2))
friendRelationRepository.save(FriendRelation(member2, member1))
}

}
Loading

0 comments on commit 57674c9

Please sign in to comment.