diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index abbbe7b..6d2182c 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -1,2 +1,1000 @@ /* 담당 D 전용 */ +.diary-list-container { + width: min(1200px, calc(100% - 80px)); + margin: 0 auto; + padding: 40px 0 80px; +} + +.diary-tabs-wrapper { + display: flex; + justify-content: center; + width: 100%; + margin-bottom: 12px; +} + +.diary-tabs { + display: inline-flex; + align-items: center; + gap: 4px; + + padding: 4px; + background: #ffffff; + border: 1px solid #dedede; + border-radius: 999px; + + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08); +} + +.diary-tab { + border: 0; + padding: 10px 18px; + background: transparent; + border-radius: 999px; + + color: #333333; + font-size: 14px; + cursor: pointer; + + text-decoration: none; +} + +.diary-tab.active { + border-color: #FBEED1; + background: #fff4cb; + color: #222222; + font-weight: 700; +} +/* +.diary-list-header { + position: relative; + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + margin-bottom: 24px; +} + +.diary-list-summary { + display: flex; + align-items: center; + gap: 10px; +} + +.diary-list-title { + margin: 0; + font-size: 26px; +} + +.diary-list-count { + padding: 5px 10px; + border-radius: 999px; + background: #fff1ba; + font-size: 13px; + font-weight: 700; +} +*/ +.diary-filter-form { + position: relative; + display: flex; + align-items: center; + gap: 8px; +} + +.diary-sort-select, +.period-toggle-button { + min-height: 42px; + padding: 0 16px; + border: 1px solid #e6ded0; + border-radius: 12px; + background: #ffffff; + color: #333333; + font: inherit; + cursor: pointer; +} + +.diary-sort-select:focus, +.period-toggle-button:focus, +.period-filter input:focus { + outline: 2px solid #e9c767; + outline-offset: 2px; +} + +.period-toggle-button.active { + border-color: #e1bd55; + background: #fff7d9; + font-weight: 700; +} + +.period-filter { + position: absolute; + z-index: 10; + top: calc(100% + 10px); + right: 0; + + display: flex; + align-items: flex-end; + gap: 10px; + + min-width: 500px; + padding: 18px; + + border: 1px solid #e6ded0; + border-radius: 16px; + background: #ffffff; + box-shadow: 0 10px 30px rgb(0 0 0 / 12%); +} + +.period-filter[hidden] { + display: none; +} + +.period-input-group { + display: flex; + flex-direction: column; + gap: 7px; +} + +.period-input-group label { + color: #666666; + font-size: 12px; +} + +.period-input-group input { + min-height: 40px; + padding: 0 10px; + border: 1px solid #ded7cb; + border-radius: 9px; + font: inherit; +} + +.period-separator { + padding-bottom: 10px; + color: #999999; +} + +.period-apply-button, +.period-reset-button { + display: inline-flex; + min-height: 40px; + align-items: center; + justify-content: center; + padding: 0 14px; + border-radius: 9px; + font: inherit; + text-decoration: none; + cursor: pointer; +} + +.period-apply-button { + border: 1px solid #e0bd59; + background: #ffe99c; + color: #222222; +} + +.period-reset-button { + border: 1px solid #ded7cb; + background: #ffffff; + color: #555555; +} + +@media (max-width: 760px) { + .diary-list-header { + align-items: flex-start; + flex-direction: column; + } + + .diary-filter-form { + width: 100%; + } +} + +/* 점 세 개 메뉴 스타일 */ + +.map-display-settings { + position: absolute; + z-index: 20; + top: 10px; + right: 10px; +} + +.map-settings-button { + display: flex; + align-items: center; + justify-content: center; + + width: 42px; + height: 42px; + + padding: 0; + border: 1px solid #dedede; + border-radius: 50%; + background: #ffffff; + + color: #444444; + font-size: 24px; + line-height: 1; + cursor: pointer; + + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); +} + +.map-settings-button:hover { + background: #f7f7f7; +} + +.map-settings-menu { + position: absolute; + top: 50px; + right: 0; + + width: 230px; + padding: 10px; + + border: 1px solid #dedede; + border-radius: 14px; + background: #ffffff; + + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.14); +} + +.map-settings-menu[hidden] { + display: none; +} + +.map-settings-title { + margin: 4px 8px 8px; + + color: #777777; + font-size: 12px; + font-weight: 700; +} + +.map-settings-menu button { + display: block; + + width: 100%; + padding: 11px 12px; + + border: 0; + border-radius: 9px; + background: transparent; + + color: #333333; + font: inherit; + text-align: left; + cursor: pointer; +} + +.map-settings-menu button:hover, +.map-settings-menu button.active { + background: #fff4cb; + font-weight: 700; +} + +/* ========================= + 감정별 화면 +========================= */ + +.emotion-page-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 30px; + margin-bottom: 30px; +} + +.emotion-month-summary { + display: flex; + align-items: center; + gap: 10px; + + padding: 12px 18px; + border: 1px solid #eadfce; + border-radius: 14px; + background: #ffffff; + + font-size: 13px; +} + +.emotion-month-summary strong { + color: #d98d36; + font-size: 18px; +} + +.emotion-bubble-area { + position: relative; + + width: 100%; + min-height: 560px; + + border-radius: 24px; + background: rgba(255, 251, 238, 0.5); +} + +.emotion-bubble { + position: absolute; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + + border: 2px solid #e8c578; + background: rgba(255, 250, 231, 0.95); + color: #333333; + font: inherit; + cursor: pointer; + + /* emotion-bubble의 각 꼭짓점의 둥근 정도 다르게 */ + border-radius: 42% 58% 53% 47% / 45% 41% 59% 49%; + + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.emotion-bubble--yellow { + background: #fff7e3; + border-color: #F9D472; +} + +.emotion-bubble--blue { + background: #eff7ff; + border-color: #DAE6F3; +} + +.emotion-bubble--pink { + background: #fff2f2; + border-color: #F8B3B4; +} + +.emotion-bubble--green { + background: #f3fff1; + border-color: #a9ce72; +} + +.emotion-bubble--default { + background: #dedede; + border-color: #c5c5c5; +} + +.emotion-bubble:hover { + z-index: 5; + + transform: scale(1.08); + + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); +} + +.emotion-bubble-name { + font-size: 14px; + font-weight: 700; +} + +.emotion-bubble-count { + color: #8e877c; + font-size: 11px; +} + +/* 기록 개수에 따른 크기 */ + +.emotion-bubble--large { + width: 130px; + height: 130px; +} + +.emotion-bubble--medium { + width: 92px; + height: 92px; +} + +.emotion-bubble--small { + width: 64px; + height: 64px; +} + +/* 임시 위치 배치 */ + +.emotion-bubble:nth-child(1) { + top: 30px; + left: 9%; +} + +.emotion-bubble:nth-child(2) { + top: 70px; + left: 25%; +} + +.emotion-bubble:nth-child(3) { + top: 160px; + left: 15%; +} + +.emotion-bubble:nth-child(4) { + top: 110px; + left: 44%; +} + +.emotion-bubble:nth-child(5) { + top: 260px; + left: 34%; +} + +.emotion-bubble:nth-child(6) { + top: 80px; + left: 68%; +} + +.emotion-bubble:nth-child(7) { + top: 210px; + left: 75%; +} + +.emotion-bubble:nth-child(8) { + top: 340px; + left: 60%; +} + +.emotion-bubble:nth-child(9) { + top: 390px; + left: 20%; +} + +.emotion-bubble:nth-child(10) { + top: 370px; + left: 80%; +} + +/* ========================= + 감정 팝업 +========================= */ + +body.modal-open { + overflow: hidden; +} + +.emotion-modal { + position: fixed; + z-index: 1000; + inset: 0; +} + +.emotion-modal[hidden] { + display: none; +} + +.emotion-modal-backdrop { + position: absolute; + inset: 0; + + background: rgba(35, 30, 25, 0.45); + backdrop-filter: blur(3px); +} + +.emotion-modal-content { + position: relative; + z-index: 1; + + width: min(1040px, calc(100% - 60px)); + max-height: calc(100vh - 80px); + margin: 40px auto; + padding: 30px; + + overflow-y: auto; + + border: 1px solid #eadfce; + border-radius: 24px; + background: #fffdf7; + + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.2); +} + +.emotion-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + + margin-bottom: 24px; +} + +.emotion-modal-label { + margin: 0 0 6px; + + color: #8d857b; + font-size: 13px; +} + +.emotion-modal-header h2 { + margin: 0; + + font-size: 28px; +} + +.emotion-modal-close { + width: 42px; + height: 42px; + + border: 1px solid #e5ddd1; + border-radius: 50%; + background: #ffffff; + + font-size: 26px; + cursor: pointer; +} + +.emotion-modal-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 20px; +} + +.emotion-modal-empty { + padding: 80px 20px; + text-align: center; +} + +@media (max-width: 900px) { + .emotion-modal-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 650px) { + .emotion-page-header { + flex-direction: column; + } + + .emotion-bubble-area { + min-height: 720px; + } + + .emotion-modal-content { + width: calc(100% - 28px); + padding: 20px; + } + + .emotion-modal-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +/* ========================= + 장소별 기록 팝업 +========================= */ + +.location-modal { + position: fixed; + z-index: 1000; + inset: 0; +} + +.location-modal[hidden] { + display: none; +} + +.location-modal-backdrop { + position: absolute; + inset: 0; + + background: rgba(35, 30, 25, 0.45); + backdrop-filter: blur(3px); +} + +.location-modal-content { + position: relative; + z-index: 1; + + width: min(1040px, calc(100% - 60px)); + max-height: calc(100vh - 80px); + margin: 40px auto; + padding: 30px; + + overflow-y: auto; + + border: 1px solid #eadfce; + border-radius: 24px; + background: #fffdf7; + + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.2); +} + +.location-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + + margin-bottom: 24px; +} + +.location-modal-label { + margin: 0 0 6px; + + color: #8d857b; + font-size: 13px; +} + +.location-modal-header h2 { + margin: 0; + font-size: 28px; +} + +.location-modal-close { + width: 42px; + height: 42px; + + border: 1px solid #e5ddd1; + border-radius: 50%; + background: #ffffff; + + font-size: 26px; + cursor: pointer; +} + +.location-modal-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 20px; +} + +.location-modal-empty { + padding: 80px 20px; + text-align: center; +} + +@media (max-width: 900px) { + .location-modal-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 650px) { + .location-modal-content { + width: calc(100% - 28px); + padding: 20px; + } + + .location-modal-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +.location-map-wrapper { + position: relative; + width: min(580px, 100%); + margin: 0 auto; +} + +.seoul-map { + display: block; + width: 100%; + height: auto; +} + +.seoul-map path { + fill: #fff; + stroke: #777; + stroke-width: 1; + + transition: + fill 0.25s ease, + stroke 0.25s ease; +} + +/* 방문한 장소 채우기 */ +.seoul-map path.has-record { + stroke: #789447; + stroke-width: 2; + cursor: pointer; +} + +.seoul-map path.has-record:hover { + filter: brightness(0.94); + stroke-width: 3; +} + +/* 지도 위 구 이름 */ +.district-labels { + pointer-events: none; +} + +.district-labels text { + fill: #444; + font-size: 25px; + font-weight: 700; + text-anchor: middle; + dominant-baseline: middle; + + paint-order: stroke; + stroke: #fff; + stroke-width: 5px; + stroke-linejoin: round; +} + +.seoul-map path { + pointer-events: all; +} + +.seoul-map path:focus { + outline: none; +} + +.seoul-map path.has-record { + cursor: pointer; +} + +.seoul-map path.has-record:hover { + filter: brightness(0.94); + stroke-width: 3; +} + +/* ========================= + 기록 카드 목록 +========================= */ + +.diary-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 22px; + width: 100%; +} + +.diary-card { + min-width: 0; + overflow: hidden; + + border: 1px solid #ebe5da; + border-radius: 14px; + background-color: #ffffff; + + box-shadow: + 0 3px 8px rgba(0, 0, 0, 0.08), + 0 1px 2px rgba(0, 0, 0, 0.04); + + transition: + transform 0.2s ease, + box-shadow 0.2s ease; +} + +.diary-card:hover { + transform: translateY(-5px); + + box-shadow: + 0 10px 20px rgba(0, 0, 0, 0.12), + 0 3px 6px rgba(0, 0, 0, 0.06); +} + +.diary-card-link { + display: block; + height: 100%; + + color: inherit; + text-decoration: none; +} + + +/* ========================= + 카드 이미지 +========================= */ + +.diary-card-image-wrapper { + position: relative; + + width: 100%; + aspect-ratio: 4 / 3; + + overflow: hidden; + background-color: #f3efe8; +} + +.diary-card-image { + display: block; + + width: 100%; + height: 100%; + + object-fit: cover; + + transition: transform 0.3s ease; +} + +.diary-card:hover .diary-card-image { + transform: scale(1.04); +} + +.diary-card-image-placeholder { + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; + + background-color: #f4f0e8; + color: #aaa397; + + font-size: 13px; +} + + +/* ========================= + 감정 표시 +========================= */ + +.diary-card-emotion { + position: absolute; + right: 10px; + bottom: 10px; + + display: flex; + align-items: center; + justify-content: center; + + min-width: 34px; + height: 34px; + padding: 0 9px; + + border: 2px solid #ffffff; + border-radius: 999px; + background-color: #fff3f3; + + color: #d65d69; + font-size: 12px; + font-weight: 700; + + box-shadow: 0 2px 7px rgba(0, 0, 0, 0.12); +} + +.diary-card-emotion img { + display: block; + + width: 28px; + height: 28px; + + object-fit: contain; +} + + +/* ========================= + 카드 내용 +========================= */ + +.diary-card-content { + display: flex; + flex-direction: column; + + min-height: 130px; + padding: 15px 14px 16px; +} + +.diary-card-date { + margin-bottom: 8px; + + color: #7f7a72; + font-size: 12px; + line-height: 1.3; +} + +.diary-card-title { + display: -webkit-box; + overflow: hidden; + + min-height: 44px; + margin: 0; + + color: #222222; + font-size: 16px; + font-weight: 700; + line-height: 1.4; + + word-break: keep-all; + + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.diary-card-location { + display: flex; + align-items: center; + gap: 5px; + + overflow: hidden; + + margin: auto 0 0; + padding-top: 13px; + + color: #7c776f; + font-size: 12px; + + text-overflow: ellipsis; + white-space: nowrap; +} + + +/* ========================= + 기록 없음 +========================= */ + +.diary-empty { + grid-column: 1 / -1; + + padding: 80px 20px; + + text-align: center; +} + +.diary-empty-title { + margin: 0 0 10px; + + color: #333333; + font-size: 20px; + font-weight: 700; +} + +.diary-empty-description { + margin: 0 0 24px; + + color: #777777; + font-size: 14px; +} + +.diary-empty-button { + display: inline-flex; + align-items: center; + justify-content: center; + + min-height: 44px; + padding: 0 20px; + + border: 1px solid #e3c566; + border-radius: 12px; + background-color: #fff1b8; + + color: #2e2a20; + font-size: 14px; + font-weight: 700; + text-decoration: none; + + transition: + transform 0.2s ease, + background-color 0.2s ease; +} + +.diary-empty-button:hover { + transform: translateY(-2px); + background-color: #ffe894; +} + + +/* ========================= + 반응형 +========================= */ + +@media (max-width: 1200px) { + .diary-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@media (max-width: 900px) { + .diary-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (max-width: 650px) { + .diary-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + } + + .diary-card-content { + min-height: 118px; + padding: 12px; + } + + .diary-card-title { + font-size: 14px; + } +} \ No newline at end of file diff --git a/diary/static/diary/js/diary.js b/diary/static/diary/js/diary.js index 5aaf506..7f39669 100644 --- a/diary/static/diary/js/diary.js +++ b/diary/static/diary/js/diary.js @@ -1,2 +1,446 @@ // 담당 D 전용 +document.addEventListener("DOMContentLoaded", () => { + const emotionColors = { + "화남": "#ef9a9a", + "분노": "#ef9a9a", + + "슬픔": "#9fc5e8", + "불안": "#9fc5e8", + "걱정": "#9fc5e8", + "우울": "#9fc5e8", + "긴장": "#9fc5e8", + "피로": "#9fc5e8", + "답답": "#9fc5e8", + "지침": "#9fc5e8", + + "기쁨": "#f8dc7d", + "행복": "#f8dc7d", + "감사": "#f8dc7d", + "감사함": "#f8dc7d", + "설렘": "#f8dc7d", + "평온": "#f8dc7d", + "안정": "#f8dc7d", + + "뿌듯": "#efb2c7", + "뿌듯함": "#efb2c7", + "성취": "#efb2c7", + "기대": "#efb2c7", + "자신감": "#efb2c7", + + "외로움": "#b9d99d", + "민망함": "#b9d99d" + }; + + const districtColors = [ + "#f3b7b7", + "#f2cf87", + "#b8d8ec", + "#c6dca7", + "#d5bee8", + "#f1bdd0", + "#abd8cf", + "#e5c0a6", + "#c8c3e8", + "#efd6a6", + "#aed7ef", + "#b8dfbe", + "#e7b9ae", + "#d4c5a5", + "#c5d8e5" + ]; + + const mapSettingsButton = + document.querySelector("#map-settings-button"); + + const mapSettingsMenu = + document.querySelector("#map-settings-menu"); + + const mapModeButtons = + document.querySelectorAll("[data-map-mode]"); + + const districtDataElements = + document.querySelectorAll( + "#district-map-data [data-district]" + ); + + + /* ========================= + 장소 팝업 요소 + ========================= */ + + const locationModal = + document.querySelector("#location-modal"); + + const locationModalName = + document.querySelector("#location-modal-name"); + + const locationModalRecords = + document.querySelectorAll(".location-modal-record"); + + const locationModalEmpty = + document.querySelector("#location-modal-empty"); + + const locationModalCloseButtons = + document.querySelectorAll( + "[data-location-modal-close]" + ); + + + /* HTML 데이터를 JavaScript 객체로 변환 */ + + function getDistrictMapData() { + return Array.from(districtDataElements).map((element) => ({ + district: element.dataset.district.trim(), + count: Number(element.dataset.count), + dominantEmotion: + element.dataset.dominantEmotion.trim(), + latestEmotion: + element.dataset.latestEmotion.trim() + })); + } + + + /* 기존 지도 색상 초기화 */ + + function clearDistrictStyles() { + document + .querySelectorAll(".seoul-map path") + .forEach((path) => { + path.style.fill = ""; + path.classList.remove("has-record"); + }); + } + + + /* 선택한 지도 색상 방식 적용 */ + + function applyMapMode(mode) { + clearDistrictStyles(); + + const mapData = getDistrictMapData(); + + const maximumCount = Math.max( + ...mapData.map((district) => district.count), + 1 + ); + + mapData.forEach((district, index) => { + const path = + document.getElementById(district.district); + + if (!path) { + console.warn( + `SVG에서 구를 찾지 못했습니다: ${district.district}` + ); + return; + } + + path.classList.add("has-record"); + + if (mode === "uniform") { + path.style.fill = "#dce9b7"; + } + + if (mode === "dominant") { + path.style.fill = + emotionColors[district.dominantEmotion] + || "#d4d4d4"; + } + + if (mode === "latest") { + path.style.fill = + emotionColors[district.latestEmotion] + || "#d4d4d4"; + } + + if (mode === "intensity") { + const minimumOpacity = 0.05; + const ratio = + district.count / maximumCount; + + const opacity = + minimumOpacity + + ratio * (1 - minimumOpacity); + + path.style.fill = + `rgba(245, 196, 80, ${opacity})`; + } + + if (mode === "rainbow") { + path.style.fill = + districtColors[ + index % districtColors.length + ]; + } + }); + + mapModeButtons.forEach((button) => { + button.classList.toggle( + "active", + button.dataset.mapMode === mode + ); + }); + + localStorage.setItem("diaryMapMode", mode); + } + + + /* ========================= + 장소 팝업 + ========================= */ + + function openLocationModal(locationName) { + if (!locationModal || !locationModalName) { + return; + } + + let visibleRecordCount = 0; + + locationModalName.textContent = locationName; + + locationModalRecords.forEach((record) => { + const recordLocation = + record.dataset.recordLocation.trim(); + + const isMatched = + recordLocation === locationName; + + record.hidden = !isMatched; + + if (isMatched) { + visibleRecordCount += 1; + } + }); + + if (locationModalEmpty) { + locationModalEmpty.hidden = + visibleRecordCount !== 0; + } + + locationModal.hidden = false; + document.body.classList.add("modal-open"); + } + + + function closeLocationModal() { + if (!locationModal) { + return; + } + + locationModal.hidden = true; + document.body.classList.remove("modal-open"); + } + + + /* 기록이 있는 구에 클릭 이벤트 연결 */ + + districtDataElements.forEach((element) => { + const districtName = + element.dataset.district.trim(); + + const districtPath = + document.getElementById(districtName); + + if (!districtPath) { + console.warn( + `클릭 이벤트를 연결할 구를 찾지 못했습니다: ${districtName}` + ); + return; + } + + /* + * tabindex를 넣지 않습니다. + * SVG에 직사각형 포커스 테두리가 나타나는 것을 방지합니다. + */ + districtPath.addEventListener("click", () => { + openLocationModal(districtName); + }); + }); + + + /* 장소 팝업 닫기 */ + + locationModalCloseButtons.forEach((button) => { + button.addEventListener( + "click", + closeLocationModal + ); + }); + + + /* ========================= + 점 세 개 메뉴 + ========================= */ + + if (mapSettingsButton && mapSettingsMenu) { + mapSettingsButton.addEventListener("click", (event) => { + event.stopPropagation(); + + const willOpen = mapSettingsMenu.hidden; + + mapSettingsMenu.hidden = !willOpen; + + mapSettingsButton.setAttribute( + "aria-expanded", + String(willOpen) + ); + }); + + mapSettingsMenu.addEventListener("click", (event) => { + event.stopPropagation(); + }); + + document.addEventListener("click", () => { + mapSettingsMenu.hidden = true; + + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + }); + } + + + /* 지도 색상 방식 선택 */ + + mapModeButtons.forEach((button) => { + button.addEventListener("click", () => { + const selectedMode = + button.dataset.mapMode; + + applyMapMode(selectedMode); + + if (mapSettingsMenu) { + mapSettingsMenu.hidden = true; + } + + if (mapSettingsButton) { + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + } + }); + }); + + + /* 페이지 진입 시 저장된 색상 모드 적용 */ + + if (districtDataElements.length > 0) { + const savedMapMode = + localStorage.getItem("diaryMapMode") + || "dominant"; + + applyMapMode(savedMapMode); + } + + const sortSelect = document.querySelector("#diary-sort"); + const filterForm = document.querySelector("#diary-filter-form"); + + const periodToggleButton = document.querySelector( + "#period-toggle-button" + ); + const periodFilter = document.querySelector("#period-filter"); + + if (sortSelect && filterForm) { + sortSelect.addEventListener("change", () => { + filterForm.submit(); + }); + } + + if (periodToggleButton && periodFilter) { + periodToggleButton.addEventListener("click", () => { + const isOpen = !periodFilter.hidden; + + periodFilter.hidden = isOpen; + periodToggleButton.classList.toggle("active", !isOpen); + periodToggleButton.setAttribute( + "aria-expanded", + String(!isOpen) + ); + }); + } + const emotionButtons = + document.querySelectorAll(".emotion-bubble"); + + const modal = + document.querySelector("#emotion-modal"); + + const modalName = + document.querySelector("#emotion-modal-name"); + + const modalRecords = + document.querySelectorAll(".emotion-modal-record"); + + const modalEmpty = + document.querySelector("#emotion-modal-empty"); + + const closeButtons = + document.querySelectorAll("[data-modal-close]"); + + function openEmotionModal(emotionId, emotionName) { + let visibleRecordCount = 0; + + modalName.textContent = emotionName; + + modalRecords.forEach((record) => { + const isMatched = + record.dataset.recordEmotion === emotionId; + + record.hidden = !isMatched; + + if (isMatched) { + visibleRecordCount += 1; + } + }); + + modalEmpty.hidden = visibleRecordCount !== 0; + modal.hidden = false; + + document.body.classList.add("modal-open"); + } + + function closeEmotionModal() { + modal.hidden = true; + document.body.classList.remove("modal-open"); + } + + emotionButtons.forEach((button) => { + button.addEventListener("click", () => { + openEmotionModal( + button.dataset.emotionId, + button.dataset.emotionName + ); + }); + }); + + closeButtons.forEach((button) => { + button.addEventListener("click", closeEmotionModal); + }); + + document.addEventListener("keydown", (event) => { + if (event.key !== "Escape") { + return; + } + + if (modal && !modal.hidden) { + closeEmotionModal(); + } + + if (locationModal && !locationModal.hidden) { + closeLocationModal(); + } + + if (mapSettingsMenu && !mapSettingsMenu.hidden) { + mapSettingsMenu.hidden = true; + + if (mapSettingsButton) { + mapSettingsButton.setAttribute( + "aria-expanded", + "false" + ); + } + } + }); +}); \ No newline at end of file diff --git a/diary/templates/diary/list.html b/diary/templates/diary/list.html index 991f6b9..1d229f0 100644 --- a/diary/templates/diary/list.html +++ b/diary/templates/diary/list.html @@ -1,3 +1,935 @@ + + {% extends "base.html" %} -{% block content %}{% endblock %} +{% load static %} + +{% block title %}다이어리 목록 | HeartMark{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+
+ + +
+ {% include "diary/partials/diary_tab.html" %} +
+ + + +
+
+
+

전체 기록

+ + {{ records|length }}개 + +
+ + +
+ + + + + + + + + +
+
+ + +
+ + +
+
+
+

감정별 기록

+

+ 감정의 크기는 이번 달 기록 횟수를 나타내요. +

+
+ +
+ 이번 달 가장 많이 기록한 감정 + {{ top_emotion.name|default:"감사" }} +
+
+ +
+ {% for emotion in emotions %} + + {% empty %} +

표시할 감정이 없어요.

+ {% endfor %} +
+
+ + +
+ + +
+
+
+ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ {% for record in records %} + + {% endfor %} +
+
+ +
+
+ + + + +{% endblock %} +{% block extra_js %} + +{% endblock %} \ No newline at end of file diff --git a/diary/templates/diary/partials/diary_tab.html b/diary/templates/diary/partials/diary_tab.html index 818d995..ed3c594 100644 --- a/diary/templates/diary/partials/diary_tab.html +++ b/diary/templates/diary/partials/diary_tab.html @@ -1,2 +1,24 @@ + \ No newline at end of file diff --git a/diary/urls.py b/diary/urls.py index 2e12300..3189f9d 100644 --- a/diary/urls.py +++ b/diary/urls.py @@ -1,5 +1,7 @@ from django.urls import path +from . import views app_name = "diary" -urlpatterns = [] - +urlpatterns = [ + path("", views.diary_list, name="list"), +] \ No newline at end of file diff --git a/diary/views.py b/diary/views.py index 6b25e2b..6b7a88e 100644 --- a/diary/views.py +++ b/diary/views.py @@ -1 +1,268 @@ # 담당 D: 캘린더 및 다이어리 목록 조회 + +from datetime import date +from django.shortcuts import render +from collections import Counter + +def diary_list(request): + selected_tab = request.GET.get("tab", "all") + selected_sort = request.GET.get("sort", "latest") + selected_start_date = request.GET.get("start_date", "") + selected_end_date = request.GET.get("end_date", "") + + records = [ + { + "id": 1, + "title": "2차 회의한 날", + "created_at": date(2026, 7, 30), + "image": None, + "emotion": { + "id": 1, + "name": "설렘", + "image": None, + }, + "location": { + "id": 1, + "name": "성북구", + }, + }, + { + "id": 2, + "title": "친구랑 카페에서 수다", + "created_at": date(2026, 7, 29), + "image": None, + "emotion": { + "id": 2, + "name": "행복", + "image": None, + }, + "location": { + "id": 2, + "name": "마포구", + }, + }, + { + "id": 3, + "title": "산책하면서 많은 생각을 했다", + "created_at": date(2026, 7, 28), + "image": None, + "emotion": { + "id": 3, + "name": "슬픔", + "image": None, + }, + "location": { + "id": 3, + "name": "성동구", + }, + }, + { + "id": 4, + "title": "작은 성공! 뿌듯한 하루", + "created_at": date(2026, 7, 27), + "image": None, + "emotion": { + "id": 4, + "name": "뿌듯함", + "image": None, + }, + "location": { + "id": 1, + "name": "성북구", + }, + }, + { + "id": 5, + "title": "노을이 너무 예뻤다", + "created_at": date(2026, 7, 26), + "image": None, + "emotion": { + "id": 2, + "name": "분노", + "image": None, + }, + "location": { + "id": 4, + "name": "강남구", + }, + }, + { + "id": 6, + "title": "마음이 편안했던 날", + "created_at": date(2026, 7, 25), + "image": None, + "emotion": { + "id": 3, + "name": "평온", + "image": None, + }, + "location": { + "id": 4, + "name": "강남구", + }, + }, + { + "id": 7, + "title": "집에서 푹 쉬었다", + "created_at": date(2026, 7, 23), + "image": None, + "emotion": { + "id": 5, + "name": "지침", + "image": None, + }, + "location": { + "id": 2, + "name": "마포구", + }, + }, + { + "id": 8, + "title": "왜인지 모르게 우울했다", + "created_at": date(2026, 7, 22), + "image": None, + "emotion": { + "id": 6, + "name": "슬픔", + "image": None, + }, + "location": { + "id": 5, + "name": "명동", + }, + }, + ] + + district_records = {} + + for record in records: + location = record.get("location") + emotion = record.get("emotion") + created_at = record.get("created_at") + + if not location or not emotion: + continue + + district_name = location.get("name") + emotion_name = emotion.get("name") + + if not district_name or not emotion_name: + continue + + # 서울 자치구만 지도에 표시 + if not district_name.endswith("구"): + continue + + if district_name not in district_records: + district_records[district_name] = [] + + district_records[district_name].append({ + "emotion_name": emotion_name, + "created_at": created_at, + }) + + + district_map_data = [] + + for district_name, district_record_list in district_records.items(): + emotion_names = [ + item["emotion_name"] + for item in district_record_list + ] + + emotion_counts = Counter(emotion_names) + dominant_emotion = emotion_counts.most_common(1)[0][0] + + latest_record = max( + district_record_list, + key=lambda item: item["created_at"] + ) + + district_map_data.append({ + "district_name": district_name, + "visit_count": len(district_record_list), + "dominant_emotion": dominant_emotion, + "latest_emotion": latest_record["emotion_name"], + }) + + emotions = [ + { + "id": 1, + "name": "감사", + "record_count": 8, + "size": "large", + }, + { + "id": 2, + "name": "설렘", + "record_count": 6, + "size": "large", + }, + { + "id": 3, + "name": "행복", + "record_count": 4, + "size": "medium", + }, + { + "id": 4, + "name": "기대", + "record_count": 3, + "size": "medium", + }, + { + "id": 5, + "name": "불안", + "record_count": 2, + "size": "small", + }, + { + "id": 6, + "name": "평온", + "record_count": 4, + "size": "medium", + }, + { + "id": 7, + "name": "뿌듯", + "record_count": 3, + "size": "medium", + }, + { + "id": 8, + "name": "지침", + "record_count": 2, + "size": "small", + }, + ] + + locations = [ + {"id": 1, "name": "성북구", "record_count": 2}, + {"id": 2, "name": "마포구", "record_count": 2}, + {"id": 3, "name": "성동구", "record_count": 1}, + {"id": 4, "name": "강남구", "record_count": 2}, + {"id": 5, "name": "명동", "record_count": 1}, + ] + + if selected_sort == "oldest": + records.sort(key=lambda record: record["created_at"]) + else: + records.sort( + key=lambda record: record["created_at"], + reverse=True, + ) + + context = { + "records": records, + "district_map_data": district_map_data, + "emotions": emotions, + "locations": locations, + "selected_tab": selected_tab, + "selected_sort": selected_sort, + "selected_start_date": selected_start_date, + "selected_end_date": selected_end_date, + "selected_emotion": request.GET.get("emotion", ""), + "selected_location": request.GET.get("location", ""), + } + + return render(request, "diary/list.html", context) \ No newline at end of file