Frond-end/Vue

[Vue3, SpringBoot, MariaDB] 이전글/ 다음글 구현하기

Nellie Kim 2024. 3. 21. 14:21
728x90

notice.xml 

이전/다음글의 idx 와 글 제목을 map으로 받았다. 

<!--	이전/다음글 조회 -->
<select id="getDetailPrevAndNextIdx" parameterType="kr.co.medical.api.notice.dto.NoticeDto"
        resultType="Map">
    SELECT
        idx,
        title
    FROM tb_notice
    WHERE idx IN (
          (SELECT idx FROM tb_notice WHERE idx <![CDATA[<]]> #{idx} AND del_yn = 'N' ORDER BY idx DESC LIMIT 1 ),
          (SELECT idx FROM tb_notice WHERE idx <![CDATA[>]]> #{idx} AND del_yn = 'N' ORDER BY idx LIMIT 1 )
        )
</select>

 

 

NoticeDto.java

@Data
public class NoticeDto extends BaseDto {
	private Integer idx;
	private String type;
	private String title;
	private String contents;
	private LocalDateTime crteDt;
	private String delYn;
	private Integer viewCount;
	private List<FileMngDto> images;
	private List<Integer> fileIdList;
	private Map<String,Object> prevAndNextIdx; // 이전글, 다음글 idx, title map

}

 

NoticeSvcImpl.java

 

noticeDao.getDetailPrevAndNextIdx(idx)로 가져온 맵을 아래와 같이 세팅해주었다. 

 public NoticeDto getDetail(Integer idx) {
        NoticeDto noticeDto = new NoticeDto();
        noticeDto.setIdx(idx);
        List<NoticeDto> noticeList = getList(noticeDto);
        if(noticeList.isEmpty()){
            throw new BizException("param_wrong");
        }else if(noticeList.size() > 1){
            throw new BizException("org.springframework.dao.DuplicateKeyException");
        }
        noticeDto = noticeList.get(0);
        List<FileMngDto> files = noticeDao.getNoticeFileList(idx);
        noticeDto.setViewCount(noticeDto.getViewCount()+1);

        // 이전/다음글의 idx, 글제목을 map에 세팅
        List<Map<String, Object>> detailPrevAndNextIdx = noticeDao.getDetailPrevAndNextIdx(idx);
        HashMap<String, Object> resultMap = new HashMap<>();
        for (Map<String, Object> map : detailPrevAndNextIdx) {
            if ((Integer) map.get("idx") < idx){
                resultMap.put("prevIdx", map.get("idx"));
                resultMap.put("prevTitle", map.get("title"));
            }else{
                resultMap.put("nextIdx", map.get("idx"));
                resultMap.put("nextTitle", map.get("title"));
            }
        }
        noticeDto.setPrevAndNextIdx(resultMap);

        noticeDao.updateViewCount(noticeDto);

        noticeDto.setImages(files.stream()
                .map(fileMngSvc::setUrlAndBytes)
                .collect(Collectors.toList()));

        return noticeDto;
    }

 

 

요청 결과 

아래와 같이 Map으로 잘 출력이 되었다. 

 

이렇게 받은 응답으로 프론트 개발을 할 것이다. 

 

 

[idx].vue

 

vue로 공지사항 상세화면을 개발했다. 

<script setup lang="ts">
import { onMounted } from 'vue'
import { getNoticeDetail, NoticeModel } from '@/apis/notice'
import { useRouter } from 'vue-router'
import { ImagePreview } from '@varlet/ui'
import { RETURN_CODE } from '@/apis/common'

/**
|--------------------------------------------------
| 😃 1. 변수
|--------------------------------------------------
*/
const isRefresh = ref(false)
const router = useRouter()
const props = defineProps<{ idx: number }>()
const noticeDetailData = ref<NoticeModel>()

/**
|--------------------------------------------------
| 🎁 2. 일반 함수
|--------------------------------------------------
*/

/** 1. url을 받아서 미리보기를 띄워주는 함수 */
function preview(url: string) {
  ImagePreview(url)
}

/** 2. 토큰 확인 함수 */
function tokenError(code: string) {
  if (/^30002\d{3}$/.test(code)) {
    Snackbar.error('로그인 후 이용 부탁드립니다')
    router.replace('/login')
  }
}

/** 3. 새로고침 함수  */
function handleRefresh() {
  isRefresh.value = false
}

/** 4. 이전글로 이동 */
function movePrevPage() {
  const prevIdx = noticeDetailData.value?.prevAndNextIdx.prevIdx
  console.log('prevIdx : ', prevIdx)
  router.replace(`/hospital/news/${prevIdx}`)
  fetchData2(prevIdx)
}

/** 5. 다음글로 이동 */
function moveNextPage() {
  const nextIdx = noticeDetailData.value?.prevAndNextIdx.nextIdx
  console.log('nextIdx : ', nextIdx)
  router.replace(`/hospital/news/${nextIdx}`)
  fetchData2(nextIdx)
}

/**
|--------------------------------------------------
| 🎁 3. fetch 함수
|--------------------------------------------------
*/
/**------------------------
  | 🎈 fetch 함수 1
  |------------------------*/
/** 1. 상세보기 호출 */
async function fetchData() {
  const idx = props.idx

  console.log('idx,,,,,,,,??', idx)
  if (idx == 0) return

  const request = {
    idx: idx
  }

  const response = await getNoticeDetail(request)

  if (response.code !== RETURN_CODE.SUCCESS) {
    console.log('정보를 가져오는 중 오류가 발생했습니다.')
    return
  }

  tokenError(response.code)

  noticeDetailData.value = response.data
  console.log('data.value,,,,,,,: ', noticeDetailData.value)
}

/**------------------------
  | 🎈 fetch 함수 2
  |------------------------*/
/** 1. 이전/다음글 클릭 시 상세보기 호출 */
async function fetchData2(idx: number) {
  console.log('idx,,,,,,,,??', idx)
  if (idx == 0) return

  const request = {
    idx: idx
  }

  const response = await getNoticeDetail(request)

  if (response.code !== RETURN_CODE.SUCCESS) {
    console.log('정보를 가져오는 중 오류가 발생했습니다.')
    return
  }

  tokenError(response.code)

  noticeDetailData.value = response.data
}

/**
|--------------------------------------------------
| 🎁 4. LifeCycle 함수
|--------------------------------------------------
*/

onMounted(() => {
  fetchData()
})
</script>

<!-- /**
|--------------------------------------------------
| 🎁 화면
|--------------------------------------------------
*/ -->
<template>
  <div class="message">
    <var-pull-refresh v-model="isRefresh" @refresh="handleRefresh">
      <app-header title="병원 소식">
        <template #left>
          <app-back />
        </template>
        <template #right>
          <app-side-menu />
        </template>
      </app-header>
      <div class="block"></div>
      <div class="content">
        <div class="message-item">
          <div class="message-list">
            <div class="message-item-detail-header-centered-box">
              -제목 : {{ noticeDetailData?.title }}<br />
              -안내 내용 : {{ noticeDetailData?.contents }}<br />

              <div v-for="(img, i) in noticeDetailData?.images" :key="i">
                <br />-이미지url(임시) {{ i + 1 }} : <br />
                <!-- <img :src="img.url" @click="preview(img.url)" /> -->
                <div>
                  {{ img.url }}
                </div>
                <img
                  src="/src/assets/images/avatar.jpg"
                  @click="preview('/src/assets/images/avatar.jpg')"
                  width="100%"
                /><br />
              </div>
              <br />
            </div>
          </div>
        </div>
      </div>
    </var-pull-refresh>
  </div>
  <br />
  <br />
  <div class="footer">
    <hr />
    <var-bottom-navigation active="">
      <var-bottom-navigation-item
        v-if="noticeDetailData?.prevAndNextIdx.prevIdx"
        label="이전글"
        icon="chevron-left"
        style="color: blueviolet; white-space: pre-line"
        @click="movePrevPage"
      />

      <var-bottom-navigation-item v-else label="이전글" icon="chevron-left" style="color: darkgrey" />

      <var-bottom-navigation-item
        v-if="noticeDetailData?.prevAndNextIdx.nextIdx"
        label="다음글"
        icon="chevron-right"
        style="color: blueviolet"
        @click="moveNextPage"
      />
      <var-bottom-navigation-item v-else label="다음글" icon="chevron-right" style="color: darkgrey" />
    </var-bottom-navigation>
    <div style="color: blueviolet; font: 0.6em sans-serif; margin: 10px">
      <span v-if="noticeDetailData?.prevAndNextIdx.prevIdx" style="margin-left: 15%; float: left">
        {{ noticeDetailData?.prevAndNextIdx.prevTitle }}
      </span>
      <span v-if="noticeDetailData?.prevAndNextIdx.nextIdx" style="margin-right: 15%; float: right">
        {{ noticeDetailData?.prevAndNextIdx.nextTitle }}
      </span>
    </div>
  </div>
</template>

<style lang="less" scoped>
.container {
  display: flex;
  flex-direction: column;
}

.content {
  padding: 20px;
}

.centered-submit {
  display: flex;
  justify-content: center;
}

.request-title {
  font-weight: bold;
  margin-bottom: 10px;
}

.nurse-textarea {
  width: 100%;
  min-height: 100px;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px;
  margin-bottom: 20px;
}

.submit-btn {
  padding: 10px 20px;
  background-color: var(--color-primary);
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
  width: 90%;
}

.request-title {
  font-weight: bold;
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.request-date {
  font-weight: normal;
  font-size: 0.9em;
}

.block {
  height: 50px;
}

.message-list {
  padding: 10px 0;
  overflow-y: auto; /* 세로 스크롤 활성화 */
  max-height: calc(100vh - var(--app-bar-height)); /* 스크롤 높이 조정 */
}

.footer {
  margin-top: 500px;
}
</style>

 

 

최종 결과물 

 

css가 안되어있으니 너무 허접하다. 

어쨌든 원했던대로, 이전글/다음글 구현도 완료하였고 아래에 해당 글의 제목이 나타나도록 구현을 완료했다.