diff --git a/diary/static/diary/css/diary.css b/diary/static/diary/css/diary.css index d4e8429..ab0ad27 100644 --- a/diary/static/diary/css/diary.css +++ b/diary/static/diary/css/diary.css @@ -1,960 +1,817 @@ /* ========================================================= - 다이어리 목록 페이지 전체 콘텐츠 영역 + 1. 다이어리 목록 페이지 전체 영역 ========================================================= */ .diary-list-container { /* - 다이어리 목록 페이지의 최대 너비를 제한합니다. + 페이지 콘텐츠의 최대 너비입니다. - min(1200px, calc(100% - 80px)) - → 화면이 넓을 때는 최대 1200px - → 화면이 좁을 때는 좌우 40px씩 여백을 남긴 너비 - - 예: - 브라우저 너비가 1400px이면 → 1200px - 브라우저 너비가 1000px이면 → 920px + 화면이 1280px 이상이면 최대 1200px, + 그보다 좁으면 좌우에 각각 40px 여백을 남깁니다. */ width: min(1200px, calc(100% - 80px)); /* - 좌우 margin을 auto로 설정해서 - 컨테이너를 화면 가운데에 배치합니다. - - 위아래 margin은 0입니다. + 컨테이너를 화면 가로 중앙에 배치합니다. */ margin: 0 auto; /* - 컨테이너 내부의 위아래 여백입니다. - - 위쪽: 28px - 좌우: 0 - 아래쪽: 50px - - 첫 번째 값 28px을 키우면 - 상단 탭과 페이지 위쪽 사이가 더 멀어집니다. - - 마지막 값 50px을 키우면 - 페이지 맨 아래 여백이 더 넓어집니다. + 위쪽 28px, 아래쪽 50px의 내부 여백입니다. */ padding: 28px 0 50px; } /* ========================================================= - 전체 / 감정별 / 장소별 탭을 감싸는 바깥 영역 + 2. 전체 / 감정별 / 장소별 탭 ========================================================= */ .diary-tabs-wrapper { /* - 내부의 탭 묶음을 정렬하기 위해 - flex 컨테이너로 설정합니다. + 탭 묶음을 가로 중앙에 배치합니다. */ display: flex; - - /* - 탭 묶음을 가로 방향 중앙에 배치합니다. - */ justify-content: center; - /* - 부모 영역의 가로 너비를 전부 사용합니다. - */ width: 100%; /* - 탭 영역 아래쪽에 12px 간격을 만듭니다. - - 값을 키우면 탭과 아래 콘텐츠 사이가 더 멀어집니다. + 탭과 아래 콘텐츠 사이의 간격입니다. */ margin-bottom: 12px; } -/* ========================================================= - 전체 / 감정별 / 장소별 탭 버튼들을 감싸는 흰색 캡슐 - ========================================================= */ - .diary-tabs { /* - 탭들을 한 줄로 배치합니다. - - inline-flex이므로 내부는 flex처럼 동작하지만, - 바깥에서는 내용 크기만큼만 너비를 차지합니다. + 각 탭 버튼을 한 줄로 배치합니다. + 내용만큼만 너비를 차지합니다. */ display: inline-flex; - - /* - 각 탭 버튼을 세로 방향 가운데에 맞춥니다. - */ align-items: center; - - /* - 각 탭 버튼 사이 간격입니다. - - 값을 키우면 전체, 감정별, 장소별 버튼 사이가 벌어집니다. - */ gap: 4px; - /* - 흰색 캡슐 내부와 탭 버튼 사이의 여백입니다. - - 값이 커질수록 전체 탭 박스가 더 두꺼워집니다. - */ padding: 4px; - /* - 탭 전체 영역의 배경색입니다. - */ - background: #ffffff; - - /* - 탭 전체 영역의 바깥 테두리입니다. - - 1px 굵기 - 실선 - 연한 회색 - */ border: 1px solid #dedede; - - /* - 매우 큰 값을 사용해서 - 탭 전체를 완전히 둥근 캡슐 모양으로 만듭니다. - */ border-radius: 999px; - /* - 탭 아래에 아주 연한 그림자를 추가합니다. + background: #ffffff; - 0 → 좌우 이동 없음 - 2px → 아래로 2px 이동 - 5px → 흐림 정도 - rgba → 검정색, 투명도 8% - */ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08); } -/* ========================================================= - 전체 / 감정별 / 장소별 각각의 탭 버튼 - ========================================================= */ - .diary-tab { /* - 기본 버튼 테두리를 제거합니다. - */ - border: 0; - - /* - 버튼 내부 여백입니다. - - 위아래: 10px - 좌우: 18px - - 좌우 값을 키우면 각 탭 버튼이 더 넓어집니다. + a 태그 또는 버튼으로 만들어진 각각의 탭입니다. */ padding: 10px 18px; - /* - 선택되지 않은 탭의 배경을 투명하게 만듭니다. - 바깥 .diary-tabs의 흰색 배경이 그대로 보입니다. - */ - background: transparent; - - /* - 각 탭 버튼도 완전히 둥근 캡슐 모양으로 만듭니다. - */ + border: 0; border-radius: 999px; - /* - 탭 글자색입니다. - */ + background: transparent; color: #333333; - /* - 탭 글자 크기입니다. - */ font-size: 14px; + text-decoration: none; - /* - 마우스를 올리면 손가락 모양 커서가 나타납니다. - */ cursor: pointer; - - /* - 탭이 a 태그로 만들어졌을 때 - 기본 밑줄을 제거합니다. - */ - text-decoration: none; } -/* ========================================================= - 현재 선택된 탭 - ========================================================= */ - .diary-tab.active { /* - 선택된 탭의 테두리 색상입니다. - - 다만 .diary-tab에서 border: 0으로 설정했기 때문에 - 현재 상태에서는 실제 테두리가 보이지 않습니다. - - 테두리를 보이게 하려면 아래처럼 별도로 설정해야 합니다. - - border: 1px solid #FBEED1; - */ - border-color: #FBEED1; - - /* - 현재 선택된 탭의 연한 노란색 배경입니다. + 현재 선택된 탭입니다. */ background: #fff4cb; - - /* - 선택된 탭의 글자색입니다. - */ color: #222222; - /* - 선택된 탭 글자를 굵게 표시합니다. - */ font-weight: 700; } -/* + +/* ========================================================= + 3. 전체 기록 제목 · 개수 · 필터 헤더 + ========================================================= */ .diary-list-header { - - 자식 요소가 absolute로 배치될 경우 - 이 요소를 기준점으로 사용하게 합니다. - - position: relative; + /* + 전체 기록 제목, 기록 개수, 필터를 + 왼쪽부터 가로 한 줄로 배치합니다. - - 제목 영역과 필터 영역을 가로로 배치합니다. - + 화면: + 전체 기록 7개 최신순 기간 설정 + */ display: flex; - - - 세로 방향 가운데 정렬합니다. - align-items: center; + justify-content: flex-start; - - 첫 번째 요소는 왼쪽, - 마지막 요소는 오른쪽에 배치합니다. - - justify-content: space-between; + /* + 제목·개수 묶음과 필터 사이의 간격입니다. - - 제목과 필터 사이의 최소 간격입니다. - - gap: 24px; + 값을 키우면 필터가 제목에서 더 멀어집니다. + */ + gap: 18px; - - 헤더와 아래 콘텐츠 사이 간격입니다. - + /* + 헤더와 카드 목록 사이의 간격입니다. + */ margin-bottom: 24px; } -.diary-list-summary { - - 제목과 기록 개수 배지를 한 줄에 배치합니다. - +.diary-list-heading { + /* + '전체 기록'과 '7개'를 같은 줄에 배치합니다. + */ display: flex; - - - 제목과 개수 배지를 세로 중앙에 맞춥니다. - align-items: center; - - - 제목과 개수 배지 사이 간격입니다. - gap: 10px; + + /* + 남는 공간을 불필요하게 차지하지 않습니다. + */ + flex: 0 0 auto; } .diary-list-title { - - h1 등의 기본 margin을 제거합니다. - + /* + h1 태그의 브라우저 기본 여백을 제거합니다. + */ margin: 0; - - 목록 제목의 글자 크기입니다. - + color: #222222; + font-size: 26px; + font-weight: 700; + line-height: 1.2; } .diary-list-count { - - 기록 개수 배지 내부 여백입니다. - - 위아래: 5px - 좌우: 10px - + /* + 기록 개수를 작은 캡슐 모양으로 표시합니다. + */ padding: 5px 10px; - - 기록 개수 배지를 캡슐 모양으로 만듭니다. - border-radius: 999px; - - 기록 개수 배지의 배경색입니다. - background: #fff1ba; + color: #333333; - - 기록 개수 글자 크기입니다. - font-size: 13px; - - - 기록 개수를 굵게 표시합니다. - font-weight: 700; -} + line-height: 1; -*/ + /* + '7'과 '개'가 서로 다른 줄로 나뉘지 않게 합니다. + */ + white-space: nowrap; +} /* ========================================================= - 최신순 / 과거순 / 기간 설정 필터 폼 + 4. 최신순 / 과거순 / 기간 설정 필터 ========================================================= */ -.diary-filter-form { - /* - 기간 설정 팝업의 absolute 위치 기준이 됩니다. - */ - position: relative; - +.diary-filter-form, +.diary-list-controls { /* - 정렬 선택창과 기간 설정 버튼을 가로로 배치합니다. + 최신순 선택창과 기간 설정 버튼을 + 같은 줄에 배치합니다. */ display: flex; + align-items: center; + gap: 8px; /* - 정렬 선택창과 버튼을 세로 중앙에 맞춥니다. + 기간 설정 팝업의 위치 기준이 됩니다. */ - align-items: center; + position: relative; /* - 정렬 선택창과 기간 설정 버튼 사이 간격입니다. + 필터 영역이 남은 가로 공간 전체를 차지하지 않습니다. */ - gap: 8px; + flex: 0 0 auto; } -/* ========================================================= - 최신순 선택창과 기간 설정 버튼의 공통 스타일 - ========================================================= */ - .diary-sort-select, .period-toggle-button { /* - 선택창과 버튼의 최소 높이입니다. - - 내부 글자 크기가 달라져도 - 최소 42px 높이를 유지합니다. + 최신순 선택창과 기간 설정 버튼의 공통 스타일입니다. */ min-height: 42px; - - /* - 위아래 여백은 0, - 좌우 여백은 16px입니다. - */ padding: 0 16px; - /* - 연한 베이지색 테두리입니다. - */ border: 1px solid #e6ded0; - - /* - 모서리를 12px만큼 둥글게 합니다. - */ border-radius: 12px; - /* - 선택창과 버튼 배경색입니다. - */ background: #ffffff; - - /* - 글자색입니다. - */ color: #333333; /* - 부모 요소에서 사용 중인 글꼴 설정을 그대로 상속합니다. - - 브라우저 기본 select, button 글꼴이 - 페이지 글꼴과 다르게 보이는 것을 방지합니다. + 페이지에서 사용하는 글꼴을 그대로 상속합니다. */ font: inherit; - /* - 클릭 가능한 요소임을 보여주는 손가락 커서입니다. - */ cursor: pointer; } -/* ========================================================= - 정렬 선택창, 기간 버튼, 날짜 입력창에 포커스가 갔을 때 - ========================================================= */ - .diary-sort-select:focus, .period-toggle-button:focus, .period-filter input:focus { /* - 키보드 탭 이동이나 클릭으로 선택됐을 때 + 키보드나 마우스로 요소를 선택했을 때 노란색 외곽선을 표시합니다. */ - outline: 2px solid #e9c767; - /* - 외곽선과 요소 사이에 2px 간격을 둡니다. - */ + outline: 2px solid #e9c767; outline-offset: 2px; + */ } -/* ========================================================= - 기간 설정 버튼이 활성화된 상태 - ========================================================= */ - .period-toggle-button.active { /* - 활성화된 버튼의 테두리를 조금 더 진한 노란색으로 바꿉니다. + 기간 설정 창이 열려 있을 때의 버튼입니다. */ border-color: #e1bd55; - /* - 활성화된 버튼의 배경을 연한 노란색으로 바꿉니다. - */ background: #fff7d9; - /* - 활성화된 버튼 글자를 굵게 표시합니다. - */ font-weight: 700; } /* ========================================================= - 기간 설정 팝업 전체 영역 + 기간 설정 팝업 ========================================================= */ .period-filter { /* - 부모인 .diary-filter-form을 기준으로 - 팝업처럼 띄워서 배치합니다. - */ - position: absolute; + 최신순 선택창과 기간 설정 버튼을 감싸는 + .diary-filter-form을 기준으로 배치됩니다. - /* - 다른 일반 요소보다 위에 표시되게 합니다. + 기존에는 필터 아래쪽에 표시되었지만, + 이제는 필터의 오른쪽에 나란히 표시합니다. */ - z-index: 10; + position: absolute; + z-index: 50; /* - 필터 폼의 아래쪽에서 10px 떨어진 위치에 배치합니다. - - 100% - → 부모 높이만큼 아래 + top: 0 + → 최신순·기간 설정 버튼과 날짜 선택창의 + 위쪽 높이를 똑같이 맞춥니다. - + 10px - → 추가 간격 + 값을 키우면 날짜 선택창이 아래로 내려가고, + 음수로 바꾸면 위로 올라갑니다. */ - top: calc(100% + 10px); + top: 0; /* - 팝업의 오른쪽 끝을 - 필터 폼의 오른쪽 끝에 맞춥니다. - */ - right: 0; + left: calc(100% + 16px) + → 필터 폼의 오른쪽 끝에서 16px 떨어진 위치에 + 날짜 선택창을 표시합니다. - /* - 날짜 입력창과 버튼들을 가로로 배치합니다. + 16px을 크게 하면 기간 설정 버튼과 더 멀어지고, + 작게 하면 버튼과 더 가까워집니다. */ - display: flex; + left: calc(100% + 16px); /* - 입력창들의 아래쪽 기준을 맞춥니다. + 기존의 right: 0을 해제합니다. - label이 있는 날짜 입력 묶음과 - 버튼의 아래쪽이 맞춰집니다. + right: 0이 남아 있으면 + left와 right가 동시에 적용되어 + 너비나 위치가 예상과 다르게 계산될 수 있습니다. */ - align-items: flex-end; + right: auto; - /* - 팝업 내부 요소 사이 간격입니다. - */ + display: flex; + align-items: flex-end; gap: 10px; /* - 팝업의 최소 너비입니다. - - 화면이 좁아도 기본적으로 500px을 유지합니다. - 모바일에서는 가로 스크롤이나 잘림이 생길 수 있습니다. + 시작일·종료일·버튼이 들어갈 최소 너비입니다. + 현재 화면에서는 약 500px 정도가 적당합니다. */ min-width: 500px; - /* - 팝업 내부 전체 여백입니다. - */ padding: 18px; - /* - 팝업 바깥 테두리입니다. - */ border: 1px solid #e6ded0; - - /* - 팝업 모서리를 둥글게 합니다. - */ border-radius: 16px; - /* - 팝업 배경색입니다. - */ background: #ffffff; - /* - 팝업 아래에 부드러운 그림자를 추가합니다. - - 0 → 좌우 이동 없음 - 10px → 아래로 10px - 30px → 흐림 정도 - 12% → 투명도 - */ box-shadow: 0 10px 30px rgb(0 0 0 / 12%); } -/* ========================================================= - hidden 속성이 있는 기간 설정 팝업 - ========================================================= */ - .period-filter[hidden] { /* - HTML에 hidden 속성이 있을 때 - 화면에서 완전히 제거합니다. + hidden 속성이 있을 때 완전히 숨깁니다. */ display: none; } -/* ========================================================= - 시작일 또는 종료일의 label + input 묶음 - ========================================================= */ - .period-input-group { /* - label과 input을 위아래로 배치합니다. + 시작일 또는 종료일의 label과 input을 + 위아래로 배치합니다. */ display: flex; - - /* - 가로가 아닌 세로 방향으로 정렬합니다. - */ flex-direction: column; - - /* - label과 input 사이 간격입니다. - */ gap: 7px; } -/* ========================================================= - 시작일 / 종료일 글자 - ========================================================= */ - .period-input-group label { - /* - label 글자색을 연한 회색으로 설정합니다. - */ color: #666666; - /* - label 글자 크기입니다. - */ font-size: 12px; } -/* ========================================================= - 날짜 선택 입력창 - ========================================================= */ - .period-input-group input { - /* - 날짜 입력창의 최소 높이입니다. - */ min-height: 40px; - - /* - 위아래 여백은 0, - 좌우 여백은 10px입니다. - */ padding: 0 10px; - /* - 날짜 입력창 테두리입니다. - */ border: 1px solid #ded7cb; - - /* - 날짜 입력창 모서리를 둥글게 합니다. - */ border-radius: 9px; - /* - 페이지의 글꼴 설정을 그대로 사용합니다. - */ + background: #ffffff; + font: inherit; } -/* ========================================================= - 시작일과 종료일 사이의 물결표(~) - ========================================================= */ - .period-separator { /* - 아래쪽 여백을 넣어 - 날짜 입력창의 가운데 높이에 가깝게 맞춥니다. + 시작일과 종료일 사이의 물결표입니다. */ padding-bottom: 10px; - /* - 구분 기호의 글자색입니다. - */ color: #999999; } -/* ========================================================= - 적용 버튼과 초기화 버튼의 공통 스타일 - ========================================================= */ - .period-apply-button, .period-reset-button { /* - flex처럼 내부 정렬이 가능하면서 - 요소 자체는 내용 크기만큼만 차지합니다. + 적용 버튼과 초기화 버튼의 공통 스타일입니다. */ display: inline-flex; - - /* - 버튼의 최소 높이입니다. - */ - min-height: 40px; - - /* - 버튼 내부 글자를 세로 가운데에 맞춥니다. - */ align-items: center; - - /* - 버튼 내부 글자를 가로 가운데에 맞춥니다. - */ justify-content: center; - /* - 위아래 여백은 0, - 좌우 여백은 14px입니다. - */ + min-height: 40px; padding: 0 14px; - /* - 버튼 모서리를 둥글게 합니다. - */ border-radius: 9px; - /* - 부모 글꼴 설정을 그대로 사용합니다. - */ font: inherit; - - /* - 초기화 버튼이 a 태그여도 - 기본 밑줄이 생기지 않게 합니다. - */ 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; +} + + /* ========================================================= - 기간 초기화 버튼 + 6. 기록 카드 목록 ========================================================= */ -.period-reset-button { +.diary-grid { /* - 초기화 버튼의 연한 회색 테두리입니다. + 기본 화면에서 한 줄에 카드 5개를 배치합니다. */ - border: 1px solid #ded7cb; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 22px; + + width: 100%; +} + +.diary-card { /* - 초기화 버튼 배경을 흰색으로 설정합니다. + 카드가 grid 칸보다 커지는 것을 방지합니다. */ - background: #ffffff; + 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 { /* - 초기화 버튼 글자색입니다. + 마우스를 올리면 카드가 살짝 위로 이동합니다. */ - color: #555555; + 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; } /* ========================================================= - 화면 너비가 760px 이하일 때 적용되는 반응형 스타일 + 7. 기록 카드 이미지 ========================================================= */ -@media (max-width: 760px) { +.diary-card-image-wrapper { + /* + 카드 이미지 영역입니다. + */ + position: relative; - .diary-list-header { - /* - 기존 가로 정렬에서 - 요소들을 왼쪽 기준으로 정렬합니다. - */ - align-items: flex-start; + width: 100%; - /* - 제목과 필터 영역을 위아래로 배치합니다. - */ - flex-direction: column; - } + /* + 이미지 영역의 가로:세로 비율을 4:3으로 유지합니다. + */ + aspect-ratio: 4 / 3; - .diary-filter-form { - /* - 모바일 화면에서 필터 폼이 - 부모 너비를 전부 차지하게 합니다. - */ - width: 100%; - } + overflow: hidden; + + background-color: #f3efe8; } -/* 점 세 개 버튼과 메뉴를 감싸는 영역 */ -.map-display-settings { - position: relative; - z-index: 30; + +.diary-card-image { + display: block; + + width: 100%; + height: 100%; + + /* + 사진이 이미지 영역을 빈틈없이 채우도록 합니다. + 넘치는 부분은 잘립니다. + */ + object-fit: cover; + + transition: transform 0.3s ease; } -.map-settings-button { + +.diary-card:hover .diary-card-image { + /* + 카드에 마우스를 올리면 사진이 살짝 확대됩니다. + */ + transform: scale(1.04); +} + + +.diary-card-image-placeholder { + /* + 사진이 없을 때 보이는 대체 영역입니다. + */ display: flex; align-items: center; justify-content: center; - width: 42px; - height: 42px; + width: 100%; + height: 100%; - padding: 0; + background-color: #f4f0e8; + color: #aaa397; - border: 1px solid #dedede; - border-radius: 50%; + font-size: 13px; +} - background: #ffffff; - color: #444444; - font-size: 24px; - line-height: 1; +/* ========================================================= + 8. 기록 카드 내용 + ========================================================= */ - cursor: pointer; +.diary-card-content { + /* + 날짜, 기록 내용, 장소 줄을 위아래로 배치합니다. + */ + display: flex; + flex-direction: column; - box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); + min-height: 130px; + padding: 15px 14px 8px; +} - transition: - background-color 0.2s ease, - transform 0.2s ease, - box-shadow 0.2s ease; + +.diary-card-date { + margin-bottom: 8px; + + color: #7f7a72; + + font-size: 12px; + line-height: 1.3; } -.map-settings-button:hover { - background: #fff8df; +.diary-card-title { /* - 버튼이 아주 조금 위로 올라가는 효과입니다. + 기록 내용을 최대 두 줄까지만 보여줍니다. */ - transform: translateY(-1px); + display: -webkit-box; - box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12); + min-height: 44px; + margin: 0; + + overflow: hidden; + + color: #222222; + + font-size: 16px; + font-weight: 700; + line-height: 1.4; + + word-break: keep-all; + + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; } -/* 키보드로 버튼을 선택했을 때 */ -.map-settings-button:focus-visible { - outline: 2px solid #e9c767; - outline-offset: 3px; + +/* ========================================================= + 9. 장소와 대표 감정 아이콘 + ========================================================= */ + +.diary-card-bottom-row { + /* + 장소는 왼쪽, + 감정 아이콘은 오른쪽에 배치합니다. + */ + display: flex; + align-items: center; + justify-content: space-between; + + /* + 장소 이름과 감정 아이콘 사이의 최소 간격입니다. + */ + gap: 12px; + + /* + 제목과 장소 줄 사이의 간격입니다. + */ + margin-top: 12px; } -/* 지도 색상 설정 메뉴 */ -.map-settings-menu { - position: absolute; +.diary-card-location { /* - 점 세 개 버튼 바로 아래에 메뉴를 배치합니다. + 장소 영역이 남은 공간을 사용합니다. */ - top: calc(100% + 8px); + flex: 1; /* - 메뉴의 오른쪽 끝과 버튼의 오른쪽 끝을 맞춥니다. + flex 자식의 글자가 정상적으로 줄어들 수 있게 합니다. */ - right: 0; + min-width: 0; - width: 230px; - padding: 10px; + margin: 0; - border: 1px solid #dedede; - border-radius: 14px; + overflow: hidden; - background: #ffffff; + color: #7c776f; - box-shadow: 0 10px 28px rgba(0, 0, 0, 0.14); + font-size: 12px; + line-height: 1.4; + + text-overflow: ellipsis; + white-space: nowrap; +} + +.diary-card-location--empty { /* - 지도보다 위에 보이지만, - 지도 영역 자체를 덮는 위치에는 있지 않습니다. + 장소 정보가 없을 때 문구를 더 연하게 표시합니다. */ - z-index: 40; + color: #aaa397; } -.map-settings-menu[hidden] { - display: none; + +.diary-card-emotion { + /* + 기존의 absolute 배치를 해제하고, + 장소 오른쪽에서 일반적인 요소로 배치합니다. + */ + position: static; + + /* + 장소 이름이 길어져도 아이콘이 작아지지 않게 합니다. + */ + flex-shrink: 0; + + display: flex; + align-items: center; + justify-content: center; } -.map-settings-title { - margin: 4px 8px 8px; - color: #777777; +.diary-card-emotion img { + display: block; - font-size: 12px; + width: 28px; + height: 28px; + + object-fit: contain; +} + + +/* ========================================================= + 10. 기록이 없을 때 + ========================================================= */ + +.diary-empty { + /* + 빈 화면 안내가 grid 전체 열을 차지하게 합니다. + */ + 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; } -/* 메뉴 안의 각 색상 방식 버튼 */ -.map-settings-menu button { - display: block; - width: 100%; - padding: 11px 12px; +.diary-empty-description { + margin: 0 0 24px; + + color: #777777; + + font-size: 14px; +} - border: 0; - border-radius: 9px; - background: transparent; - color: #333333; +.diary-empty-button { + display: inline-flex; + align-items: center; + justify-content: center; - font: inherit; - text-align: left; + min-height: 44px; + padding: 0 20px; - cursor: pointer; -} + border: 1px solid #e3c566; + border-radius: 12px; -/* 메뉴 항목 위에 마우스를 올렸을 때 */ -.map-settings-menu button:hover { - background: #fff7d7; -} + background-color: #fff1b8; + color: #2e2a20; -/* 현재 선택된 지도 색상 방식 */ -.map-settings-menu button.active { - background: #fff1ba; + font-size: 14px; font-weight: 700; + text-decoration: none; + + transition: + transform 0.2s ease, + background-color 0.2s ease; } -/* ========================= - 감정별 화면 -========================= */ -/* 가장 많이 기록한 감정들의 전체 영역 */ -.top-emotion-list { - display: flex; - align-items: center; - justify-content: flex-end; - flex-wrap: wrap; - gap: 4px; -} +.diary-empty-button:hover { + transform: translateY(-2px); -/* 각 공동 1위 감정 이름 */ -.top-emotion-name { - color: #d98d36; - white-space: nowrap; + background-color: #ffe894; } -/* 감정과 감정 사이의 구분점 */ -.top-emotion-separator { - color: #b7aa97; -} + +/* ========================================================= + 11. 감정별 기록 화면 + ========================================================= */ .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; } + +.top-emotion-list { + /* + 공동 1위 감정이 여러 개일 경우 + 한 줄 또는 여러 줄로 자연스럽게 표시 + */ + display: flex; + align-items: center; + justify-content: flex-end; + flex-wrap: wrap; + gap: 4px; +} + + +.top-emotion-name { + color: #d98d36; + + white-space: nowrap; +} + + +.top-emotion-separator { + color: #b7aa97; +} + + .emotion-bubble-area { + /* + 감정 조약돌들의 absolute 배치 기준입니다. + */ position: relative; width: 100%; min-height: 560px; border-radius: 24px; - background: #fff; + + /*background: #ffffff;*/ } + .emotion-bubble { position: absolute; @@ -964,283 +821,236 @@ justify-content: center; gap: 4px; - border: 2px solid #e8c578; + /* + 기존 테두리를 제거 + */ + border: none; + + /* + 조약돌 모양은 그대로 유지 + */ + border-radius: 42% 58% 53% 47% / 50% 50% 50% 50%; + background: rgba(255, 250, 231, 0.95); color: #333333; + + /* + 테두리 대신 기본 상태에서도 그림자를 표시합니다. + + 0 → 좌우 이동 없음 + 5px → 아래로 5px 이동 + 14px → 그림자 흐림 정도 + rgba → 검정색, 투명도 12% + */ + box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12); + 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:hover { + z-index: 5; + + transform: scale(1.08); + + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); +} + + +.emotion-bubble-name { + font-size: 20px; + font-weight: 700; +} + + +.emotion-bubble-count { + color: #8e877c; + + font-size: 15px; +} + + +/* 감정별 색상 */ + .emotion-bubble--yellow { + /*border-color: #f9d472;*/ + background: #fff7e3; - border-color: #F9D472; } + .emotion-bubble--blue { + /*border-color: #dae6f3;*/ + background: #eff7ff; - border-color: #DAE6F3; } + .emotion-bubble--pink { + /*border-color: #f8b3b4;*/ + background: #fff2f2; - border-color: #F8B3B4; } + .emotion-bubble--purple { + /*border-color: #9d72ce;*/ + background: #f8f1ff; - border-color: #9d72ce; } + .emotion-bubble--red { + /*border-color: #ce7772;*/ + background: #fff4f1; - border-color: #ce7772; } + .emotion-bubble--orange { - background: #fff9f1; - border-color: #cea972; -} + /*border-color: #cea972;*/ -.emotion-bubble--default { - background: #dedede; - border-color: #c5c5c5; + background: #fff9f1; } -.emotion-bubble:hover { - z-index: 5; - - transform: scale(1.08); - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12); -} +.emotion-bubble--default { + /*border-color: #c5c5c5;*/ -.emotion-bubble-name { - font-size: 14px; - font-weight: 700; + background: #dedede; } -.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:nth-child(10) { + top: 370px; + left: 80%; +} - .emotion-bubble-area { - min-height: 720px; - } - .emotion-modal-content { - width: calc(100% - 28px); - padding: 20px; - } +/* ========================================================= + 12. 감정별 기록 팝업 + ========================================================= */ - .emotion-modal-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } +body.modal-open { + /* + 팝업이 열렸을 때 뒤쪽 페이지가 스크롤되지 않게 합니다. + */ + overflow: hidden; } -/* ========================= - 장소별 기록 팝업 -========================= */ -.location-modal { +.emotion-modal { position: fixed; z-index: 1000; + inset: 0; } -.location-modal[hidden] { + +.emotion-modal[hidden] { display: none; } -.location-modal-backdrop { + +.emotion-modal-backdrop { position: absolute; + inset: 0; background: rgba(35, 30, 25, 0.45); + backdrop-filter: blur(3px); } -.location-modal-content { + +.emotion-modal-content { position: relative; z-index: 1; width: min(1040px, calc(100% - 60px)); max-height: calc(100vh - 80px); + margin: 40px auto; padding: 30px; @@ -1248,12 +1058,14 @@ body.modal-open { border: 1px solid #eadfce; border-radius: 24px; + background: #fffdf7; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.2); } -.location-modal-header { + +.emotion-modal-header { display: flex; align-items: flex-start; justify-content: space-between; @@ -1261,424 +1073,488 @@ body.modal-open { margin-bottom: 24px; } -.location-modal-label { + +.emotion-modal-label { margin: 0 0 6px; color: #8d857b; + font-size: 13px; } -.location-modal-header h2 { + +.emotion-modal-header h2 { margin: 0; + font-size: 28px; } -.location-modal-close { + +.emotion-modal-close { width: 42px; height: 42px; border: 1px solid #e5ddd1; border-radius: 50%; + background: #ffffff; font-size: 26px; + cursor: pointer; } -.location-modal-grid { + +.emotion-modal-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 20px; } -.location-modal-empty { + +.emotion-modal-empty { padding: 80px 20px; - text-align: center; -} -@media (max-width: 900px) { - .location-modal-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } + text-align: center; } -@media (max-width: 650px) { - .location-modal-content { - width: calc(100% - 28px); - padding: 20px; - } - .location-modal-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} +/* ========================================================= + 13. 장소별 기록 화면 및 지도 설정 + ========================================================= */ .location-map-section { /* - 툴바와 지도를 위아래로 배치합니다. + 지도 설정 툴바와 지도를 위아래로 배치합니다. */ display: flex; flex-direction: column; - - /* - 설정 버튼 영역과 지도 사이 간격입니다. - 값을 키우면 버튼과 지도가 더 멀어집니다. - */ gap: 10px; width: 100%; } -/* ========================= - 지도 위쪽 도구 영역 -========================= */ .map-toolbar { /* 점 세 개 버튼을 오른쪽 끝에 배치합니다. */ display: flex; - justify-content: flex-end; align-items: center; + justify-content: flex-end; width: 100%; } -/* ========================= - 실제 지도 영역 -========================= */ -.location-map-wrapper { +.map-display-settings { /* - 지도 내부 SVG 배치의 기준이 됩니다. - 현재는 설정 메뉴가 지도 바깥에 있으므로 - 반드시 필요하지는 않지만 다른 요소 배치에 유용합니다. + 설정 메뉴가 점 세 개 버튼을 기준으로 표시되게 합니다. */ position: relative; - - width: min(800px, 100%); - margin: 0 auto; + z-index: 30; } -.seoul-map { - display: block; - width: 100%; - height: auto; -} -.seoul-map path { - fill: #fff; - stroke: #777; - stroke-width: 1; +.map-settings-button { + display: flex; + align-items: center; + justify-content: center; - transition: - fill 0.25s ease, - stroke 0.25s ease; -} + width: 42px; + height: 42px; + padding: 0; + + border: 1px solid #dedede; + border-radius: 50%; + + background: #ffffff; + color: #444444; + + font-size: 24px; + line-height: 1; -/* 방문한 장소 채우기 */ -.seoul-map path.has-record { - /*stroke: #789447;*/ - stroke-width: 2; cursor: pointer; + + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1); + + transition: + background-color 0.2s ease, + transform 0.2s ease, + box-shadow 0.2s ease; } -.seoul-map path.has-record:hover { - filter: brightness(0.94); - stroke-width: 3; + +.map-settings-button:hover { + transform: translateY(-1px); + + /*background: #fff8df;*/ + + box-shadow: 0 5px 14px rgba(0, 0, 0, 0.12); } -/* 지도 위 구 이름 */ -.district-labels { - pointer-events: none; + +.map-settings-button:focus-visible { + outline: 2px solid #e9c767; + outline-offset: 3px; } -.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; +.map-settings-menu { + /* + 점 세 개 버튼 바로 아래에 표시되는 설정 메뉴입니다. + */ + position: absolute; + z-index: 40; + + top: calc(100% + 8px); + 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); } -.seoul-map path { - pointer-events: all; + +.map-settings-menu[hidden] { + display: none; } -.seoul-map path:focus { - outline: none; + +.map-settings-title { + margin: 4px 8px 8px; + + color: #777777; + + font-size: 12px; + font-weight: 700; } -.seoul-map path.has-record { + +.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; } -.seoul-map path.has-record:hover { - filter: brightness(0.94); - stroke-width: 3; + +.map-settings-menu button:hover { + background: #fff7d7; } -/* ========================= - 기록 카드 목록 -========================= */ -.diary-grid { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - gap: 22px; +.map-settings-menu button.active { + background: #fff1ba; + + font-weight: 700; +} + + +/* ========================================================= + 14. 서울 지도 + ========================================================= */ + +.location-map-wrapper { + /* + 서울 지도 SVG의 크기와 가운데 정렬을 담당합니다. + */ + position: relative; + + width: min(800px, 100%); + + margin: 0 auto; +} + + +.seoul-map { + display: block; + width: 100%; + height: auto; } -.diary-card { - min-width: 0; - overflow: hidden; - border: 1px solid #ebe5da; - border-radius: 14px; - background-color: #ffffff; +.seoul-map path { + /* + 모든 서울 구역 path의 기본 스타일입니다. + */ + fill: #ffffff; + stroke: #777777; + stroke-width: 1; + + pointer-events: all; + + transition: + fill 0.25s ease, + stroke 0.25s ease; +} + + +.seoul-map path:focus { + /* + SVG 구역을 클릭했을 때 + 기본 브라우저 외곽선을 제거합니다. + */ + outline: none; +} + - box-shadow: - 0 3px 8px rgba(0, 0, 0, 0.08), - 0 1px 2px rgba(0, 0, 0, 0.04); +.seoul-map path.has-record { + /* + 기록이 있는 구역에 적용됩니다. + */ + stroke-width: 2; - transition: - transform 0.2s ease, - box-shadow 0.2s ease; + cursor: pointer; } -.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); +.seoul-map path.has-record:hover { + /* + 기록이 있는 구역에 마우스를 올렸을 때 + 살짝 어두워지고 테두리가 굵어집니다. + */ + /*stroke-width: 3;*/ + + filter: brightness(0.94); } -.diary-card-link { - display: block; - height: 100%; - color: inherit; - text-decoration: none; +.district-labels { + /* + 구 이름 텍스트가 지도 클릭을 방해하지 않게 합니다. + */ + pointer-events: none; } -/* ========================= - 카드 이미지 -========================= */ +.district-labels text { + fill: #444444; + stroke: #ffffff; + stroke-width: 5px; + stroke-linejoin: round; -.diary-card-image-wrapper { - position: relative; + font-size: 25px; + font-weight: 700; - width: 100%; - aspect-ratio: 4 / 3; + text-anchor: middle; + dominant-baseline: middle; - overflow: hidden; - background-color: #f3efe8; + /* + 흰색 외곽선을 먼저 그리고 + 그 위에 글자를 표시합니다. + */ + paint-order: stroke; } -.diary-card-image { - display: block; - width: 100%; - height: 100%; +/* ========================================================= + 15. 장소별 기록 팝업 + ========================================================= */ - object-fit: cover; +.location-modal { + position: fixed; + z-index: 1000; - transition: transform 0.3s ease; + inset: 0; } -.diary-card:hover .diary-card-image { - transform: scale(1.04); + +.location-modal[hidden] { + display: none; } -.diary-card-image-placeholder { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; +.location-modal-backdrop { + position: absolute; - background-color: #f4f0e8; - color: #aaa397; + inset: 0; - font-size: 13px; + background: rgba(35, 30, 25, 0.45); + + backdrop-filter: blur(3px); } -/* ========================= - 감정 표시 -========================= */ +.location-modal-content { + position: relative; + z-index: 1; -.diary-card-emotion { - position: absolute; - right: 10px; - bottom: 10px; + width: min(1040px, calc(100% - 60px)); + max-height: calc(100vh - 80px); - display: flex; - align-items: center; - justify-content: center; + margin: 40px auto; + padding: 30px; - min-width: 34px; - height: 34px; - padding: 0 9px; + overflow-y: auto; - border: 2px solid #ffffff; - border-radius: 999px; - background-color: #fff3f3; + border: 1px solid #eadfce; + border-radius: 24px; - color: #d65d69; - font-size: 12px; - font-weight: 700; + background: #fffdf7; - box-shadow: 0 2px 7px rgba(0, 0, 0, 0.12); + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.2); } -.diary-card-emotion img { - display: block; - width: 28px; - height: 28px; +.location-modal-header { + display: flex; + align-items: flex-start; + justify-content: space-between; - object-fit: contain; + margin-bottom: 24px; } -/* ========================= - 카드 내용 -========================= */ - -.diary-card-content { - display: flex; - flex-direction: column; - - min-height: 130px; - padding: 15px 14px 16px; -} +.location-modal-label { + margin: 0 0 6px; -.diary-card-date { - margin-bottom: 8px; + color: #8d857b; - color: #7f7a72; - font-size: 12px; - line-height: 1.3; + font-size: 13px; } -.diary-card-title { - display: -webkit-box; - overflow: hidden; - min-height: 44px; +.location-modal-header h2 { 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; + font-size: 28px; } -.diary-card-location { - display: flex; - align-items: center; - gap: 5px; - overflow: hidden; +.location-modal-close { + width: 42px; + height: 42px; - margin: auto 0 0; - padding-top: 13px; + border: 1px solid #e5ddd1; + border-radius: 50%; - color: #7c776f; - font-size: 12px; + background: #ffffff; - text-overflow: ellipsis; - white-space: nowrap; + font-size: 26px; + + cursor: pointer; } -/* ========================= - 기록 없음 -========================= */ +.location-modal-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 20px; +} -.diary-empty { - grid-column: 1 / -1; +.location-modal-empty { padding: 80px 20px; text-align: center; } -.diary-empty-title { - margin: 0 0 10px; - color: #333333; - font-size: 20px; - font-weight: 700; +/* ========================================================= + 16. 반응형 스타일 + ========================================================= */ + +/* + 화면 너비가 1200px 이하일 때 + 카드 개수를 한 줄에 4개로 줄입니다. +*/ +@media (max-width: 1200px) { + .diary-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } } -.diary-empty-description { - margin: 0 0 24px; - color: #777777; - font-size: 14px; +/* + 화면 너비가 900px 이하일 때 + 카드와 팝업 목록을 한 줄에 3개로 줄입니다. +*/ +@media (max-width: 900px) { + .diary-grid, + .emotion-modal-grid, + .location-modal-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } } -.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; +/* + 화면 너비가 760px 이하일 때 + 전체 기록 제목과 필터를 위아래로 배치합니다. +*/ +@media (max-width: 760px) { + .diary-list-container { + /* + 작은 화면에서는 좌우 여백을 각각 20px로 줄입니다. + */ + width: calc(100% - 40px); + } - color: #2e2a20; - font-size: 14px; - font-weight: 700; - text-decoration: none; + .diary-list-header { + align-items: flex-start; + flex-direction: column; + gap: 14px; + } - transition: - transform 0.2s ease, - background-color 0.2s ease; -} + .diary-filter-form, + .diary-list-controls { + width: 100%; + } -.diary-empty-button:hover { - transform: translateY(-2px); - background-color: #ffe894; -} + .period-filter { + /* + 작은 화면에서는 오른쪽에 표시할 공간이 부족하므로 + 다시 필터 버튼 아래쪽에 표시합니다. + */ + top: calc(100% + 10px); + /* + 필터 폼의 왼쪽에 날짜창을 맞춥니다. + */ + left: 0; + right: auto; -/* ========================= - 반응형 -========================= */ + /* + 날짜 입력창과 버튼이 공간이 부족할 때 + 다음 줄로 내려가도록 합니다. + */ + flex-wrap: wrap; -@media (max-width: 1200px) { - .diary-grid { - grid-template-columns: repeat(4, minmax(0, 1fr)); + /* + 모바일 화면보다 날짜창이 커지지 않게 제한합니다. + */ + min-width: min(500px, calc(100vw - 40px)); } } -@media (max-width: 900px) { - .diary-grid { - grid-template-columns: repeat(3, minmax(0, 1fr)); - } -} +/* + 화면 너비가 650px 이하일 때 + 카드 및 팝업 목록을 한 줄에 2개로 줄입니다. +*/ @media (max-width: 650px) { - .diary-grid { + .diary-grid, + .emotion-modal-grid, + .location-modal-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } @@ -1691,4 +1567,18 @@ body.modal-open { .diary-card-title { font-size: 14px; } + + .emotion-page-header { + flex-direction: column; + } + + .emotion-bubble-area { + min-height: 720px; + } + + .emotion-modal-content, + .location-modal-content { + width: calc(100% - 28px); + padding: 20px; + } } \ No newline at end of file diff --git a/diary/static/diary/images/emotion-01.svg b/diary/static/diary/images/emotion-01.svg deleted file mode 100644 index e64804c..0000000 --- a/diary/static/diary/images/emotion-01.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/diary/static/diary/js/diary.js b/diary/static/diary/js/diary.js index 81ed21e..2808cd0 100644 --- a/diary/static/diary/js/diary.js +++ b/diary/static/diary/js/diary.js @@ -1,6 +1,11 @@ // 담당 D 전용 document.addEventListener("DOMContentLoaded", () => { + + /* ========================================================= + 1. 지도 감정 색상 정보 + ========================================================= */ + const emotionColors = { "심술": "#ef9a9a", "화남": "#ef9a9a", @@ -27,6 +32,11 @@ document.addEventListener("DOMContentLoaded", () => { "냉철": "#b3b2db", }; + + /* + 지도에서 '구별로 다른 색' 모드를 선택했을 때 + 각 구에 순서대로 적용할 색상 목록입니다. + */ const districtColors = [ "#f3b7b7", "#f2cf87", @@ -45,24 +55,52 @@ document.addEventListener("DOMContentLoaded", () => { "#c5d8e5" ]; + + /* ========================================================= + 2. 지도 설정 요소 + ========================================================= */ + + /* + 지도 오른쪽 위의 점 세 개 버튼입니다. + */ const mapSettingsButton = document.querySelector("#map-settings-button"); + + /* + 점 세 개 버튼을 눌렀을 때 열리는 + 지도 색상 설정 메뉴입니다. + */ const mapSettingsMenu = document.querySelector("#map-settings-menu"); + + /* + 지도 색상 방식을 선택하는 버튼들입니다. + + 예: + - 같은 색으로 통일 + - 가장 많은 감정 색 + - 가장 최근 감정 색 + - 방문 횟수에 따른 농도 + - 구별로 다른 색 + */ const mapModeButtons = document.querySelectorAll("[data-map-mode]"); + + /* + Django가 HTML에 넣어둔 구별 기록 데이터입니다. + */ const districtDataElements = document.querySelectorAll( "#district-map-data [data-district]" ); - /* ========================= - 장소 팝업 요소 - ========================= */ + /* ========================================================= + 3. 장소별 기록 팝업 요소 + ========================================================= */ const locationModal = document.querySelector("#location-modal"); @@ -82,75 +120,142 @@ document.addEventListener("DOMContentLoaded", () => { ); - /* HTML 데이터를 JavaScript 객체로 변환 */ + /* ========================================================= + 4. 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() })); } - /* 기존 지도 색상 초기화 */ + /* ========================================================= + 5. 기존 지도 색상 초기화 + ========================================================= */ function clearDistrictStyles() { document .querySelectorAll(".seoul-map path") .forEach((path) => { + + /* + JavaScript로 직접 넣었던 fill 색상을 제거합니다. + */ path.style.fill = ""; + + + /* + 기록이 있는 구에 붙었던 클래스를 제거합니다. + */ path.classList.remove("has-record"); }); } - /* 선택한 지도 색상 방식 적용 */ + /* ========================================================= + 6. 선택한 지도 색상 방식 적용 + ========================================================= */ function applyMapMode(mode) { + + /* + 새로운 색상을 적용하기 전에 + 이전 지도 색상을 먼저 초기화합니다. + */ clearDistrictStyles(); + + /* + HTML에 담긴 구별 데이터를 배열로 가져옵니다. + */ const mapData = getDistrictMapData(); + + /* + 방문 횟수 농도를 계산하기 위해 + 가장 방문 횟수가 많은 구의 기록 수를 구합니다. + + 기록이 없을 때 0으로 나누는 문제를 막기 위해 + 최소값을 1로 설정합니다. + */ const maximumCount = Math.max( ...mapData.map((district) => district.count), 1 ); + mapData.forEach((district, index) => { + + /* + district.district와 같은 id를 가진 + 서울 지도 SVG path를 찾습니다. + + 예: + district.district가 "성북구"라면 + id="성북구"인 path를 찾습니다. + */ 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; @@ -162,6 +267,10 @@ document.addEventListener("DOMContentLoaded", () => { `rgba(245, 196, 80, ${opacity})`; } + + /* + 각 구에 서로 다른 색상을 순서대로 적용합니다. + */ if (mode === "rainbow") { path.style.fill = districtColors[ @@ -170,6 +279,11 @@ document.addEventListener("DOMContentLoaded", () => { } }); + + /* + 현재 선택한 지도 색상 방식 버튼에만 + active 클래스를 추가합니다. + */ mapModeButtons.forEach((button) => { button.classList.toggle( "active", @@ -177,23 +291,45 @@ document.addEventListener("DOMContentLoaded", () => { ); }); + + /* + 새로고침한 뒤에도 선택한 지도 색상 방식이 + 유지되도록 브라우저 저장소에 저장합니다. + */ localStorage.setItem("diaryMapMode", mode); } - /* ========================= - 장소 팝업 - ========================= */ + /* ========================================================= + 7. 장소별 기록 팝업 열기 + ========================================================= */ function openLocationModal(locationName) { + + /* + 장소 팝업 요소가 없는 페이지에서는 + 아래 코드를 실행하지 않습니다. + */ if (!locationModal || !locationModalName) { return; } + + /* + 선택한 장소와 일치하는 기록의 개수입니다. + */ let visibleRecordCount = 0; + + /* + 팝업 제목에 선택한 구 이름을 넣습니다. + */ locationModalName.textContent = locationName; + + /* + 해당 구에 작성된 기록만 표시합니다. + */ locationModalRecords.forEach((record) => { const recordLocation = record.dataset.recordLocation.trim(); @@ -208,27 +344,48 @@ document.addEventListener("DOMContentLoaded", () => { } }); + + /* + 일치하는 기록이 없다면 + 빈 기록 안내 문구를 표시합니다. + */ if (locationModalEmpty) { locationModalEmpty.hidden = visibleRecordCount !== 0; } + + /* + 장소별 기록 팝업을 엽니다. + */ locationModal.hidden = false; + + + /* + 팝업이 열린 동안 뒤쪽 페이지 스크롤을 막습니다. + */ document.body.classList.add("modal-open"); } + /* ========================================================= + 8. 장소별 기록 팝업 닫기 + ========================================================= */ + function closeLocationModal() { if (!locationModal) { return; } locationModal.hidden = true; + document.body.classList.remove("modal-open"); } - /* 기록이 있는 구에 클릭 이벤트 연결 */ + /* ========================================================= + 9. 기록이 있는 서울 구역 클릭 이벤트 + ========================================================= */ districtDataElements.forEach((element) => { const districtName = @@ -237,16 +394,21 @@ document.addEventListener("DOMContentLoaded", () => { const districtPath = document.getElementById(districtName); + if (!districtPath) { console.warn( `클릭 이벤트를 연결할 구를 찾지 못했습니다: ${districtName}` ); + return; } + /* - * tabindex를 넣지 않습니다. - * SVG에 직사각형 포커스 테두리가 나타나는 것을 방지합니다. + tabindex는 넣지 않습니다. + + SVG를 클릭했을 때 직사각형 포커스 테두리가 + 나타나는 문제를 방지합니다. */ districtPath.addEventListener("click", () => { openLocationModal(districtName); @@ -254,7 +416,9 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* 장소 팝업 닫기 */ + /* ========================================================= + 10. 장소별 기록 팝업 닫기 버튼 + ========================================================= */ locationModalCloseButtons.forEach((button) => { button.addEventListener( @@ -264,28 +428,65 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* ========================= - 점 세 개 메뉴 - ========================= */ + /* ========================================================= + 11. 지도 점 세 개 설정 메뉴 + ========================================================= */ if (mapSettingsButton && mapSettingsMenu) { + + /* + 점 세 개 버튼을 누르면 + 지도 설정 메뉴를 열거나 닫습니다. + */ mapSettingsButton.addEventListener("click", (event) => { + + /* + 버튼 클릭이 document 클릭 이벤트까지 + 전달되는 것을 막습니다. + + 이 코드가 없으면 메뉴를 열자마자 + 아래 document 클릭 이벤트가 실행되어 + 다시 닫힐 수 있습니다. + */ event.stopPropagation(); + + /* + 현재 메뉴가 숨겨져 있다면 true입니다. + */ const willOpen = mapSettingsMenu.hidden; + + /* + 숨겨져 있었다면 열고, + 열려 있었다면 닫습니다. + */ mapSettingsMenu.hidden = !willOpen; + + /* + 접근성 상태도 메뉴 상태와 맞춰 변경합니다. + */ mapSettingsButton.setAttribute( "aria-expanded", String(willOpen) ); }); + + /* + 설정 메뉴 내부를 클릭했을 때는 + document 클릭 이벤트로 전달되지 않게 합니다. + */ mapSettingsMenu.addEventListener("click", (event) => { event.stopPropagation(); }); + + /* + 지도 설정 메뉴 바깥을 클릭하면 + 설정 메뉴를 닫습니다. + */ document.addEventListener("click", () => { mapSettingsMenu.hidden = true; @@ -297,15 +498,29 @@ document.addEventListener("DOMContentLoaded", () => { } - /* 지도 색상 방식 선택 */ + /* ========================================================= + 12. 지도 색상 방식 선택 + ========================================================= */ mapModeButtons.forEach((button) => { button.addEventListener("click", () => { + + /* + 클릭한 버튼의 data-map-mode 값을 가져옵니다. + */ const selectedMode = button.dataset.mapMode; + + /* + 선택한 방식으로 지도 색상을 변경합니다. + */ applyMapMode(selectedMode); + + /* + 색상 방식 선택 후 설정 메뉴를 닫습니다. + */ if (mapSettingsMenu) { mapSettingsMenu.hidden = true; } @@ -320,7 +535,9 @@ document.addEventListener("DOMContentLoaded", () => { }); - /* 페이지 진입 시 저장된 색상 모드 적용 */ + /* ========================================================= + 13. 페이지 진입 시 저장된 지도 색상 방식 적용 + ========================================================= */ if (districtDataElements.length > 0) { const savedMapMode = @@ -330,13 +547,43 @@ document.addEventListener("DOMContentLoaded", () => { 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"); + /* ========================================================= + 14. 전체 기록 정렬 및 기간 설정 요소 + ========================================================= */ + + /* + 최신순·과거순 선택창입니다. + */ + 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"); + + + /* ========================================================= + 15. 최신순·과거순 선택 시 자동 제출 + ========================================================= */ if (sortSelect && filterForm) { sortSelect.addEventListener("change", () => { @@ -344,18 +591,168 @@ document.addEventListener("DOMContentLoaded", () => { }); } + + /* ========================================================= + 16. 기간 설정창 열기 + ========================================================= */ + + function openPeriodFilter() { + + /* + 현재 페이지에 기간 설정 요소가 없다면 + 함수를 종료합니다. + */ + if (!periodToggleButton || !periodFilter) { + return; + } + + + /* + hidden 속성을 제거해 기간 설정창을 표시합니다. + */ + periodFilter.hidden = false; + + + /* + 기간 설정 버튼에 active 클래스를 추가합니다. + + CSS의 + .period-toggle-button.active + 스타일이 적용됩니다. + */ + periodToggleButton.classList.add("active"); + + + /* + 스크린 리더에 설정창이 열렸다는 것을 전달합니다. + */ + periodToggleButton.setAttribute( + "aria-expanded", + "true" + ); + } + + + /* ========================================================= + 17. 기간 설정창 닫기 + ========================================================= */ + + function closePeriodFilter() { + + /* + 현재 페이지에 기간 설정 요소가 없다면 + 함수를 종료합니다. + */ + if (!periodToggleButton || !periodFilter) { + return; + } + + + /* + hidden 속성을 추가해 기간 설정창을 숨깁니다. + */ + periodFilter.hidden = true; + + + /* + 버튼의 활성화 스타일을 제거합니다. + */ + periodToggleButton.classList.remove("active"); + + + /* + 스크린 리더에 설정창이 닫혔다는 것을 전달합니다. + */ + periodToggleButton.setAttribute( + "aria-expanded", + "false" + ); + } + + + /* ========================================================= + 18. 기간 설정 버튼 클릭 + ========================================================= */ + if (periodToggleButton && periodFilter) { periodToggleButton.addEventListener("click", () => { - const isOpen = !periodFilter.hidden; - periodFilter.hidden = isOpen; - periodToggleButton.classList.toggle("active", !isOpen); - periodToggleButton.setAttribute( - "aria-expanded", - String(!isOpen) - ); + /* + 버튼을 누르기 전에 기간 설정창이 + 숨겨져 있었는지 확인합니다. + + true + → 현재 닫혀 있으므로 열어야 합니다. + + false + → 현재 열려 있으므로 닫아야 합니다. + */ + const shouldOpen = periodFilter.hidden; + + + if (shouldOpen) { + openPeriodFilter(); + } else { + closePeriodFilter(); + } + }); + + + /* ===================================================== + 19. 기간 설정창 바깥 클릭 시 닫기 + ===================================================== */ + + document.addEventListener("click", (event) => { + + /* + 실제로 클릭된 HTML 요소입니다. + */ + const clickedElement = event.target; + + + /* + 클릭한 위치가 기간 설정 버튼 내부인지 확인합니다. + + 버튼의 글자 부분을 클릭해도 true가 됩니다. + */ + const clickedToggleButton = + periodToggleButton.contains(clickedElement); + + + /* + 클릭한 위치가 기간 설정창 내부인지 확인합니다. + + 다음 요소를 클릭했을 때 true가 됩니다. + + - 시작 날짜 입력창 + - 종료 날짜 입력창 + - 적용 버튼 + - 초기화 버튼 + - 기간 설정창의 빈 공간 + */ + const clickedInsidePeriodFilter = + periodFilter.contains(clickedElement); + + + /* + 기간 설정 버튼도 아니고, + 기간 설정창 내부도 아니라면 + 팝업 바깥을 클릭한 것입니다. + */ + if ( + !clickedToggleButton + && !clickedInsidePeriodFilter + ) { + closePeriodFilter(); + } }); } + + + /* ========================================================= + 20. 감정별 기록 팝업 요소 + ========================================================= */ + const emotionButtons = document.querySelectorAll(".emotion-bubble"); @@ -374,11 +771,34 @@ document.addEventListener("DOMContentLoaded", () => { const closeButtons = document.querySelectorAll("[data-modal-close]"); + + /* ========================================================= + 21. 감정별 기록 팝업 열기 + ========================================================= */ + function openEmotionModal(emotionId, emotionName) { + + /* + 감정별 기록 팝업 요소가 없다면 + 함수를 실행하지 않습니다. + */ + if (!modal || !modalName || !modalEmpty) { + return; + } + + let visibleRecordCount = 0; + + /* + 팝업 제목에 선택한 감정 이름을 넣습니다. + */ modalName.textContent = emotionName; + + /* + 선택한 감정과 일치하는 기록만 표시합니다. + */ modalRecords.forEach((record) => { const isMatched = record.dataset.recordEmotion === emotionId; @@ -390,17 +810,45 @@ document.addEventListener("DOMContentLoaded", () => { } }); + + /* + 표시할 기록이 없다면 빈 안내 문구를 표시합니다. + */ modalEmpty.hidden = visibleRecordCount !== 0; + + + /* + 감정별 기록 팝업을 표시합니다. + */ modal.hidden = false; + + /* + 팝업 뒤쪽 페이지의 스크롤을 막습니다. + */ document.body.classList.add("modal-open"); } + + /* ========================================================= + 22. 감정별 기록 팝업 닫기 + ========================================================= */ + function closeEmotionModal() { + if (!modal) { + return; + } + modal.hidden = true; + document.body.classList.remove("modal-open"); } + + /* ========================================================= + 23. 감정 조약돌 클릭 + ========================================================= */ + emotionButtons.forEach((button) => { button.addEventListener("click", () => { openEmotionModal( @@ -410,23 +858,53 @@ document.addEventListener("DOMContentLoaded", () => { }); }); + + /* ========================================================= + 24. 감정 팝업 닫기 버튼 및 배경 클릭 + ========================================================= */ + closeButtons.forEach((button) => { - button.addEventListener("click", closeEmotionModal); + button.addEventListener( + "click", + closeEmotionModal + ); }); + + /* ========================================================= + 25. Escape 키로 열린 요소 닫기 + ========================================================= */ + document.addEventListener("keydown", (event) => { + + /* + 누른 키가 Escape가 아니라면 + 아래 코드를 실행하지 않습니다. + */ if (event.key !== "Escape") { return; } + + /* + 감정별 기록 팝업이 열려 있다면 닫습니다. + */ if (modal && !modal.hidden) { closeEmotionModal(); } + + /* + 장소별 기록 팝업이 열려 있다면 닫습니다. + */ if (locationModal && !locationModal.hidden) { closeLocationModal(); } + + /* + 지도 점 세 개 설정 메뉴가 열려 있다면 닫습니다. + */ if (mapSettingsMenu && !mapSettingsMenu.hidden) { mapSettingsMenu.hidden = true; @@ -437,5 +915,13 @@ document.addEventListener("DOMContentLoaded", () => { ); } } + + + /* + 기간 설정창이 열려 있다면 닫습니다. + */ + if (periodFilter && !periodFilter.hidden) { + closePeriodFilter(); + } }); }); \ No newline at end of file diff --git a/diary/templates/diary/list.html b/diary/templates/diary/list.html index e0ec338..39224df 100644 --- a/diary/templates/diary/list.html +++ b/diary/templates/diary/list.html @@ -14,10 +14,10 @@ {% comment %} 다이어리 목록 페이지 전용 스타일을 불러옵니다. {% endcomment %} - {% endblock %} + {% block content %}
@@ -26,7 +26,7 @@
{% include "diary/partials/diary_tab.html" %}
- +
-
-

전체 기록

+ + +
+

+ 전체 기록 +

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