블로그 UI 수정

- 리스트와 작성글 3:7 비율로 변경
 - 리스트 뷰 10개 넘어가면 pageState 추가

gitblog 블로그 일상 카테고리 글들 이전
This commit is contained in:
2026-01-18 13:30:07 +09:00
parent 8462557ee3
commit 370cd4fca0
14 changed files with 839 additions and 21 deletions

View File

@@ -0,0 +1,20 @@
---
title: 일상 기록 첫 번째 이야기
date: 2022-03-11
tags: 일상
excerpt: 일상 기록 첫 번째 이야기
---
# 일상 기록 첫 번째 이야기
~~깃~~ 블로그를 만들면서 앞으로 써 내려갈 일상 디렉토리의 첫 번째 포스트를 작성해 봅니다.
어려서부터 내 생각을 정리하거나 메모를 할 때에는 간단히 들고 다니는 메모장이나 노트에 많이 끄적여 봤는데,
이처럼 누구나 볼 수 있는 페이지에 나의 일상을 기록하고 내 생각을 정리하는 포스트를 만든다는게 너무 어색하기만 하다 :)
평소에 글을 쓰는걸 별로 좋아하지 않았기 때문에 글을 작성하는데에도 많은 시간이 소요되고 내용도 알차지는 못하겠지만, 점점 나의 글을 쓰면서 어제보다 발전해가는 모습을 볼 수 있지 않을까?!
때문에 일상 포스트를 꼭 해보고 싶었다.
~~깃~~ 블로그 일상 포스트 시작합니다.

View File

@@ -0,0 +1,20 @@
---
title: Transferring git storage
date: 2022-03-13
tags: 일상
excerpt: 깃 저장소 변경하면서 잠깐 쉬는 타임에 끄적여보는 일상 기록
---
# 일상 기록 - 깃 저장소 변경
기존 학부시절부터 사용했던 깃 저장소([git_storage_gahu](https://github.com/gahu))에서 했었던 프로젝트와 공부 했던 기록들을 정리해서
새로운 깃 저장소([git_storage_gahusb](https://github.com/gahusb))로 변경하였다.
기존의 깃이 공부하면서 썻던 내용들이라... 정리도 되어 있지 않았고 그래서 뭔가 지저분하게 되어 있는데 어디서부터 손을 대야 할지 감이 잡히지 않았다.
때문에 새로운 [저장소](https://github.com/gahusb)를 만들고, github io를 이용하여 나의 [블로그](https://gahusb.github.io/)도 작성하면서
기존 프로젝트들의 내용 정리와 블로그 정리 두가지를 같이 하면서 옮기게 되었다.
블로그 작업 준비도 하면서 기존 프로젝트들을 옮기고 정리한다는게 쉬운 일은 아니지만
지금 목표로 하고 있는 일이 있기 때문에 시작을 할 수 있었던것 같고,
무엇보다도 새로 산 Macbook M1 Max를 잘 활용해 볼 수 있는 기회가 되지 않을까 생각하면서 작업하고 있다.

View File

@@ -0,0 +1,18 @@
---
title: 오랜만에 삼겹살집!
date: 2022-03-15
tags: 일상
excerpt: 오랜만에 삼겹살집에 가서 먹부림한 하루 기록.
---
# 일상 기록 - 삼겹살 먹부림
오늘 오랜만에 집 근처에 있는 삼겹살집에 갔다.
퇴근 후 평소와는 다른 방향으로 걸어가면서 삼겹살이 먹고 싶다던 승토리와 함께 돼지고기 뿌시러 갔다.
해바라기집이라는 이곳은 이 집에 온지 얼마 안 지나서 방문하고 이번이 두 번째 방문이다.
![image](/assets/img/daily/20220315_191531.jpg)
돼지 반마리를 시켰는데, 위에 사진 처럼 두꺼운 삼겹살과 항정살, 갈비살, 목살 등이 500g 나오는데 우리 둘이서는 이정도가 딱 정당히 배부르고 좋은 것 같다.
최근에 감기 기운이 있어서 목도 칼칼하니 별로 좋지 않았는데 기름진 고기를 먹으며 힐링했던 시간

View File

@@ -0,0 +1,49 @@
---
title: 소고기 먹부림
date: 2022-04-02
tags: 일상
excerpt: 여의도 창고 43에서 오랜만에 소고기와 와인을 즐긴 기록.
---
# 일상 기록 - 소는 누가~~ 키우나?!
오랜만에 승토리와 몸보신(?)을 하기 위해서 여의도에 있는 고깃집을 탐색했다.
여의도에는 소고기 오마카세를 하는 집도 있었지만 오랜만에 소고기를 소금에 찍어서 먹어보고 싶다는 의견에 나도 적극적으로 동의했고, 오마카세가 아닌 구이집을 가기로 했다.
약속시간, 7시 반 이라는 조금 늦은 저녁이 되어서 배고픔을 안고 여의도로 넘어왔고, 가깝지만 괜찮다고 생각되는 집으로 향했다.
## 창고 43 본점
여의도 맨하탄빌딩 2층에 있는 창고 43으로 갔다.
![image](/assets/img/daily/%EC%B0%BD%EA%B3%A043-1%20Medium.jpeg)
입구에 가까히 갈수록 소고기 특유의 냄새가 풍겼고, 발걸음을 더 빨리해서 입성!
예약을 하지 않았지만 다행히도 조금 늦은 시간에 가서 그런지 한 타임의 폭풍이 지나가고 난 뒤의 모습이었다.
덕분에 바로 자리를 안내 받을 수 있었다.
가장 괜찮아보이는 한우 세트를 주문하고 함께 어울릴만한 와인도 하나 먹고 싶었다!
오늘은 FLEX 하는 날!!
무난한 평의 칠레 산 와인을 시켜보았다.
![image](/assets/img/daily/%EC%B0%BD%EA%B3%A043-2%20Medium.jpeg)
살짝 무게감이 있으면서도 향이 아주 좋아서 우리 둘다 만족할 수 있었다.
그렇게 와인을 한 잔 따라놓고 가볍게 향을 즐기고 있을 때쯤!
드디어!!! 소고기님 입장하셨다.
![image](/assets/img/daily/%EC%B0%BD%EA%B3%A043-3%20Medium.jpeg)
사진으로 보기에는 양이 적어 보일 순 있지만
맞다.😥 뭔가 평소 먹었던 양보다는 확실히 적긴 했지만 마블링이 살아 있었다...
투쁠 안심, 새우살, 투쁠 등심 이렇게 세트로 하는데 600g이었나..?
그래도 처음으로 소에서 가장 적게 나와서 귀하다는 새우살을 먹어 볼 수 있다는 생각에 들떠 있었고, 그 맛은 너무 훌륭했다.😋
입에 넣자마자 사라지는 마법을 느끼며 정말 괜찮은 소고기를 먹어본 것 같다.
고기와 와인을 먹고 냉면, 깍두기 볶음밥까지 완벽하게 마무리를 지으며 배 터지게 잘 먹고서 나왔다. :)

View File

@@ -0,0 +1,263 @@
---
title: 블로그 기능 추가
date: 2022-04-07
tags: 개발
excerpt: slick 슬라이더, 목차, 페이지 버튼, 유튜브 임베드 추가 정리.
---
# 게시글 테스트
* toc
{:toc .large-only}
---
## slick image
[Slick](https://kenwheeler.github.io/slick/)
slick 사이트에 들어가서 `get it now`를 눌러 다운로드 받아주어 slick 폴더를 `/assets/css/slick`에 복사>붙여넣기 해준다.
게시글 원하는 위치에 아래와 같이 넣어주면 된다.
```html
<div class="main_center">
<div><img src= "/assets/img/blog/hydejack-8.jpg" style="width: auto; height: 500px;"></div>
<div><img src="/assets/img/blog/hydejack-8.png" style="width: auto; height: 500px;"></div>
<div><img src= "/assets/img/blog/hydejack-9-dark.jpg" style="width: auto; height: 500px;"></div>
</div>
<script>
$(document).ready(function() {
$('.main_center').slick({
autoplay : true, /*자동으로 슬라이딩됨*/
dots : true, /* 하단 점 버튼 */
speed : 300 /* 이미지가 슬라이딩시 걸리는 시간 */,
infinite : true,
autoplaySpeed : 30000 /* 이미지가 다른 이미지로 넘어 갈때의 텀 */,
arrows : true,
slidesToShow : 1,
slidesToScroll : 1,
touchMove : true, /* 마우스 클릭으로 끌어서 슬라이딩 가능여부 */
nextArrows : true, /* 넥스트버튼 */
prevArrows : true,
arrow : true, /*false면 좌우 버튼 없음, true면 좌우 버튼 보임*/
fade : false
});
});
</script>
```
.slick() 안의 옵션을 원하는대로 설정하여 사용 할 수 있다.
<div class="main_center">
<div><img src= "/assets/img/blog/hydejack-8.jpg" style="width: auto; height: 500px;"></div>
<div><img src="/assets/img/blog/hydejack-8.png" style="width: auto; height: 500px;"></div>
<div><img src= "/assets/img/blog/hydejack-9-dark.jpg" style="width: auto; height: 500px;"></div>
</div>
<script>
$(document).ready(function() {
$('.main_center').slick({
autoplay : true, /*자동으로 슬라이딩됨*/
dots : true, /* 하단 점 버튼 */
speed : 300 /* 이미지가 슬라이딩시 걸리는 시간 */,
infinite : true,
autoplaySpeed : 30000 /* 이미지가 다른 이미지로 넘어 갈때의 텀 */,
arrows : true,
slidesToShow : 1,
slidesToScroll : 1,
touchMove : true, /* 마우스 클릭으로 끌어서 슬라이딩 가능여부 */
nextArrows : true, /* 넥스트버튼 */
prevArrows : true,
arrow : true, /*false면 좌우 버튼 없음, true면 좌우 버튼 보임*/
fade : false
});
});
</script>
그리고 scss를 수정하여 좌우로 화살표를 넣어서 표출 하는것도 가능하도록 변경 할 수 있다.
이미지는 별도로 구해서 사용하시면 됩니다.
> assests/css/slick/slick-theme.css
```css
.slick-prev:before
{
content: url(left.png);
}
[dir='rtl'] .slick-prev:before
{
content: url(right.png);
}
.slick-next:before
{
content: url(right.png);
}
[dir='rtl'] .slick-next:before
{
content: url(left.png);
}
```
---
## 게시글 목차 만들기
h1 타이틀 바로 아래에
```
* toc
{:toc .large-only}
```
를 추가하여 헤더를 기준으로 목차 생성
---
## 페이지 버튼 만들기
> _include/components/page-button.html
```html
<div class="page-control">
<div>
{% if page.previous.url %}
<a id="prev" class="w-btn-outline w-btn-gray-outline" href="{{ page.previous.url }}">&laquo; {{ page.previous.title }}</a>
{% endif %}
</div>
<div>
{% if page.next.url %}
<a id="next" class="w-btn-outline w-btn-gray-outline" href="{{ page.next.url }}">{{ page.next.title }} &raquo;</a>
{% endif %}
</div>
</div>
```
> _layouts/post.html
```console
{% include components/page-button.html %}
```
About 위에 위치하여 작성자 소개 위에 위치하도록 한다.
원하는 스타일대로 변경을 위해서 다음과 같이 작업해준다.
> _sass/my-style.scss
```scss
.page-control {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-control > div {
max-width: 50%;
}.page-control {
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
}
.page-control > div {
max-width: 50%;
}
.w-btn-outline {
position: relative;
padding: 15px 30px;
border-radius: 15px;
font-family: "paybooc-Light", sans-serif;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
text-decoration: none;
font-weight: 600;
transition: 0.25s;
}
.w-btn-outline:hover {
letter-spacing: 2px;
transform: scale(1.2);
cursor: pointer;
}
.w-btn-outline:active {
transform: scale(1.5);
}
.w-btn-gray-outline {
border: 3px solid #a3a1a1;
color: #6e6e6e;
}
.w-btn-gray-outline:hover {
background-color: #a3a1a1;
color: #e3dede;
}
```
---
## 게시글에 유튜브플레이어 보기
> _includes/components/youtubePlayer.html
```html
<style>
.embed-container { position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; }
.embed-container iframe,
.embed-container object,
.embed-container embed { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
</style>
<div class="embed-container" >
<iframe src="https://www.youtube.com/embed/{{ include.id }}" frameborder="0" allowfullscreen="" onclick="ga('send', 'event', 'post', 'click', 'youtubePlayer');">
</iframe>
</div>
```
위 처럼 youtube 영상의 id를 가져와서 표출 할 수 있도록 만들고
```console
`{% include components/youtubePlayer.html id="{youtube ID}" %}`
```
게시글 원하는 위치에 youtube id를 넣어주면 된다.
![image](https://likelion.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F72612a68-022b-424b-8ee1-4600408d2b93%2FUntitled.png?table=block&id=b894acab-60c6-4de6-b0c2-7e0c15a7c844&spaceId=c69962b0-3951-485b-b10a-5bb29576bba8&width=1220&userId=&cache=v2)
위와 같이 `watch?v=` 이후에 오는 id를 가져다가 사용한다.
{% include components/youtubePlayer.html id='0TNFb5zgpbg' %}
---
## 게시물 조회수 보이기
Hits 이용
> [Hits](https://hits.seeyoufarm.com/)
TARGET URL에 나의 블로그 주소를 입력하고, Options에서 여러 설정들을 조정하여 내가 원하는 스타일의 hits를 만들 수 있다. 미리보기를 통해 표시되는것을 볼 수 있으니 참고해서 만들면 된다.
그렇게 생성된 아래의 HTML Link를 복사해서
> _layouts/post.html
에 넣어서 각 게시물 별로 조회수가 보이는 위치를 조정할 수 있다.
> _includes/body/sidebar-sticky.html
에 넣으면 사이드바는 어떤 게시물을 봐도 같이 보이는것이기 때문에 전체 조회수를 체크할 수 있다.
만약 조회수 가장 오른쪽에 생성되는 링크모양 아이콘이 싫다면
> _sass/my-style.scss
```scss
a.external::after, a::after {
display: none;
}
```
를 추가하여 제거 할 수 있다.
---

View File

@@ -0,0 +1,24 @@
---
title: 코딩테스트
date: 2022-04-09
tags: 공부
excerpt: 오늘의집 코딩테스트를 치르며 느낀 점을 정리한 기록.
---
# 코딩 테스트 기록
오늘의집 개발자 대규모 채용 코딩테스트
오늘은 오늘의집이라는 유니콘 기업에 프론트엔드 개발자 포지션으로 코딩테스트를 했다.
문제는 3문제 180분이었고, 나는 평소대로 JAVA로 치뤘다.
3문제 모두 문자열과 관련된 알고리즘을 사용해 푸는 문제였고, 1번 문제는 단순히 문자열에서 다음에 나오는 문자를 보고 방향전환 및 직진 거리에 대한 명령을 알려주는 프로그램이라 단순 구현력을 묻는 문제였다.
2번 문제는 반복 문자열을 제거하는 알고리즘 문제로, 이 문제와 유사한 문제를 이전에 풀었었는데.. 이게 참 내 생각처럼 쉽게 해결되지 않아서 이 문제에만 1시간 반 가까이 소비한 것 같다.
3번 문제는 문자열에 특정 변수를 두고 그것을 치환하는 문제였는데, HashSet을 이용해서 반복 유무 검사와 무한 반복인지를 체크하도록 하여 해결하였다.
지금까지 알고리즘을 이렇게 꾸준히 한 경험이 많지 않아서인지 생각보다 구현에는 어려움이 없었고, 평소에 얼마나 하느냐가 크게 다가왔었다. ~~(심지어 전날 새벽까지 놀고 집중해서 할 수 있었다는게 놀라울 정도)~~
목표인 IT 대장급이라고 불리는 기업의 코테 올솔할 수 있는 실력을 갖출때까지 멈추지 않고 계속 할 것이다.

View File

@@ -0,0 +1,16 @@
---
title: 일상기록 - 04월25일
date: 2022-04-25
tags: 일상
excerpt: 바쁜 일정 속에서 다시 루틴을 잡아가려는 마음 기록.
---
# 일상의 기록 - 04월25일
최근에는 업무도 바빠서 늦게까지 야근하고, 그러지 않은 날에는 미뤄진 약속을 가느라고 목표로 했던 1일 1commit 100일 목표에서 많이 떨어지게 된 것 같다.
물론 채우기 위해서 중간중간 알고리즘 한 문제라도 풀어서 올리고는 있지만 예전만큼 활발하지 못한것 같아서 거의 반성의 기록을 남긴다.
처음 1일 1commit을 목표로 했을 때에는 공부도 흥미롭고, 일도 여유가 있었기 때문에 공부에 투자하는 시간도 많았는데 그러지 못하는게 너무 불편한 상황이 되었다.
이번주만 지나면 그래도 여유를 찾을 수 있을것 같으니 좀 더 공부하고 목표를 채워가는 일상을 기록 할 수 있도록 노력할 것이다.

View File

@@ -0,0 +1,19 @@
---
title: 식의약 공공데이터 활용 공모전 준비
date: 2022-06-09
tags: 개발, 아이디어
excerpt: 공공데이터 공모전 준비 과정과 아이디어 선정 기록.
---
# 식의약 공공데이터 활용 공모전 준비
이번에 같은 팀의 동료와 동기 3명이서 식의약 공공데이터 활용을 해서 웹/앱 개발을 하는 공모전에 참가하려고 한다. <br />
그래서 사전 Notion을 이용하여 일정 관리 및 아이디어 모집을 했고, 오늘에서 모여 최종 아이디어 회의를 시작했다.
다양한 아이디어들 중에서 3가지 아이디어가 가장 괜찮을것 같았고, 그 중 내가 제시한 아이디어가 가장 참신성에서 괜찮다고 투표 결과가 나와서 채택하게 되었다. <br />
아이디어가 정해지고 나서도 활용할 공공데이터의 데이터 내용 및 배경, 기대효과, Mock-up design 등 해야 할 것들이 많이 있어서 정리하는데만 시간이 더 걸리기도 했다. <br/>
퇴근 후에 다들 힘든 상태에서 진행하기도 했고, 이번 주.. 너무 바빴어서 잠도 제대로 잔 적이 없긴 하지만 오랜만에 다시 두근거리는 프로젝트를 진행하는것 같아서 기대가 된다. <br />
(개발하느라 고생할거 생각하면 아찔하긴 하다..) <br />
8월 초까지 개발이 어느정도 완료가 되어야 하니 스케줄 정해진대로 완료해서 좋은 성과를 냈으면 한다. <br />

View File

@@ -0,0 +1,17 @@
---
title: 식의약 공공데이터 활용 공모전 1차 평가 통과
date: 2022-07-06
tags: 개발, 아이디어
excerpt: 공모전 1차 통과 후 일정과 해야 할 일 정리.
---
# 식의약 공공데이터 활용 공모전 1차 평가 통과
'그린라이트'라는 장기 기증 활성화를 주제로 하는 캠페인의 이름을 따서 식의약 공공데이터 활용 공모전에 지난 6월에 참가를 했었다. <br/>
아이디어만큼은 자신이 있었으나 마이너한듯한 분야라서 걱정을 하고 있었는데, 오늘 1차 평가 통과 메세지가 왔다!!
67개의 팀이 참가해서 아이디어부문 8개 팀, 개발부문 4개 팀이 선정 되었는데 그 중 개발부문 한 팀으로 선정이 된 것이다!!! <br/>
기쁨도 잠시... 2차 평가의 날이 보름정도 남은 상태라 발표자료와 앱 개발을 해야하는 상황이 온 것이다. <br/>
공공데이터도 사용 신청도 미흡한 상태라 걱정이 되긴 하지만 팀원들과 모여서 합의하고 일정 및 업무 분담을 하기로 했다.
이번에도 좋은 결과를 거둘 수 있는 공모전이 되었으면 좋겠다고 생각하고 있다.

View File

@@ -0,0 +1,31 @@
---
title: daily diary
date: 2023-04-29
tags: 일상
excerpt: 오랜만에 기록을 다시 시작하며 하루를 정리한 글.
---
# 일상의 기록
오랫만에 일상의 기록
23년에는 한 번도 기록을 하지 않았더라구..
물론 23년에는 승토리와 처음으로 해외 여행도 다녀오고 연휴도 있었고 이직을 하기 때문에 회사일에 집중하기도 했다.
그래도 너무 오랜만에 MacBook Pro를 켜면서 켜져 있는 VScode 창을 보면서 끝내지 못한 포스팅도 있었고, 기록이 22년 12월을 마지막으로 멈춰 있는 상태를 발견하고나니 반성하자 나 자신..
오늘은 승토리와 함께 아침 일찍부터 낙성대역 근처 카페에 자리잡고 각자 해야 할 일을 하고서,
저녁에는 이사가야 할 집 구경도 하고 가구의 치수도 재는 등 하려고 나왔다.
물론 내 생각보다는 너무 늦게 나온편이긴 하지만 이 정도면 선방했지!
5월 19일에 이사도 가고 새로운 직장에 조금씩 익숙해지면 다시 본격적으로 꾸준히 업데이트하며 공부할 계획을 다시 세워보겠다!
서버 개설 및 유지, 관리와 같은 부분에서 많이 부족하다는 것을 느끼고 있고, 확실히 그동안 해왔던 임베디드와는 전혀 다른 성격을 지나고 있기 때문에 서버 모니터링 & VOC와 같은 내용들을 따라가는게 어렵긴 하다.
그와 관련해서 내 NAS 서버를 관리하면서 그런 부족한 부분을 채워가보면 어떨까 지금은 생각하고 있다.
우선은 이사에 집중하고 워크스페이스 관리를 좀 더 신경써서 주기적으로 내 스스로의 가치를 올릴 수 있도록 해보겠다.
그리고 요새는 부업과 관련된 내용을 나름대로 정리하고 있다.
세상에는 내가 모르던 분야에서 다양한 부업으로 제 2의 수익을 내는 방법이 많은것을 알고서 깜짝 놀랐고,
그런 방법을 습득하고 소소하게라도 제 2의 수입을 안정적으로 만들 수 있는 노력을 할 예정이다.
또한 승토리와 6월에 결혼을 준비하는 단계에 도입하기로 했다.
이것은 엄청나게 의미 있는 것이며, 인생에서 전환기가 될 수 있다고 생각한다.
지금 이사가는 집에서 같이 시작하기에는 내가 생각했던 부분보다는 소소할 수 있겠지만, 차근차근 준비해보려고 한다. 💪

View File

@@ -1,7 +1,7 @@
--- ---
title: 로또 실험실을 조금 더 재미있게 title: 로또 실험실을 조금 더 재미있게
date: 2026-01-12 date: 2026-01-12
tags: product, lotto tags: 개발, product, lotto
excerpt: 작은 실험으로 시작한 로또 페이지를 앞으로 어떻게 발전시키려는지 정리했습니다. excerpt: 작은 실험으로 시작한 로또 페이지를 앞으로 어떻게 발전시키려는지 정리했습니다.
--- ---

View File

@@ -1,7 +1,7 @@
--- ---
title: 새 블로그를 열었습니다 title: 새 블로그를 열었습니다
date: 2026-01-18 date: 2026-01-18
tags: intro, blog tags: 일상, intro, blog
excerpt: 이제부터 개발 기록과 여행 기록을 이곳에 차곡차곡 쌓아갑니다. excerpt: 이제부터 개발 기록과 여행 기록을 이곳에 차곡차곡 쌓아갑니다.
--- ---

View File

@@ -48,7 +48,7 @@
.blog-grid { .blog-grid {
display: grid; display: grid;
grid-template-columns: minmax(0, 0.45fr) minmax(0, 0.55fr); grid-template-columns: minmax(0, 0.3fr) minmax(0, 0.7fr);
gap: 22px; gap: 22px;
align-items: start; align-items: start;
} }
@@ -58,6 +58,27 @@
gap: 12px; gap: 12px;
} }
.blog-category-filter {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.blog-category-chip {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.04);
color: var(--text);
border-radius: 999px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
}
.blog-category-chip.is-active {
border-color: rgba(247, 168, 165, 0.6);
background: rgba(247, 168, 165, 0.2);
}
.blog-list__item { .blog-list__item {
border: 1px solid var(--line); border: 1px solid var(--line);
background: var(--surface); background: var(--surface);
@@ -79,6 +100,34 @@
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
} }
.blog-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding-top: 10px;
}
.blog-page-btn {
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.05);
color: var(--text);
border-radius: 999px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
}
.blog-page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.blog-page-indicator {
color: var(--muted);
font-size: 12px;
}
.blog-list__title { .blog-list__title {
margin: 0; margin: 0;
font-weight: 600; font-weight: 600;
@@ -170,6 +219,31 @@
font-size: 0.85em; font-size: 0.85em;
} }
.blog-article__body a {
color: #f7d4c9;
}
.md-image {
width: 100%;
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
margin: 12px 0;
}
.md-quote {
margin: 0 0 14px;
padding: 12px 16px;
border-left: 3px solid rgba(247, 168, 165, 0.6);
background: rgba(255, 255, 255, 0.03);
color: var(--muted);
}
.md-hr {
border: none;
border-top: 1px solid rgba(255, 255, 255, 0.14);
margin: 18px 0;
}
.md-code { .md-code {
padding: 14px; padding: 14px;
border-radius: 12px; border-radius: 12px;
@@ -183,6 +257,68 @@
color: var(--muted); color: var(--muted);
} }
.blog-categories {
display: grid;
gap: 18px;
}
.blog-categories__head h2 {
margin: 0 0 6px;
font-size: 24px;
font-family: var(--font-display);
}
.blog-categories__head p {
margin: 0;
color: var(--muted);
}
.blog-categories__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 14px;
}
.blog-category-card {
border: 1px solid var(--line);
background: var(--surface);
border-radius: 18px;
padding: 16px;
text-align: left;
color: inherit;
cursor: pointer;
display: grid;
gap: 12px;
transition: border-color 0.2s ease;
}
.blog-category-card:hover {
border-color: rgba(255, 255, 255, 0.25);
}
.blog-category-card__head {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
}
.blog-category-card__count {
font-size: 12px;
color: var(--muted);
}
.blog-category-card__list {
display: grid;
gap: 6px;
color: var(--muted);
font-size: 13px;
}
.blog-category-card__empty {
color: var(--muted);
}
@media (max-width: 900px) { @media (max-width: 900px) {
.blog-header, .blog-header,
.blog-grid { .blog-grid {

View File

@@ -1,12 +1,45 @@
import React, { useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { getBlogPosts } from '../../data/blog'; import { getBlogPosts } from '../../data/blog';
import './Blog.css'; import './Blog.css';
const renderInline = (text) => { const renderInline = (text) => {
const pattern = /(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g; const normalized = text.replace(/<br\s*\/?>/gi, '\n');
const parts = text.split(pattern).filter(Boolean); const pattern =
/(!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/g;
const segments = normalized.split('\n');
return parts.map((part, index) => { return segments.flatMap((segment, segmentIndex) => {
const parts = segment.split(pattern).filter(Boolean);
const rendered = parts.map((part, index) => {
if (part.startsWith('![')) {
const match = part.match(/!\[([^\]]*)\]\(([^)]+)\)/);
if (!match) return <span key={`${part}-${index}`}>{part}</span>;
const [, alt, src] = match;
return (
<img
key={`${part}-${index}`}
src={src}
alt={alt}
className="md-image"
loading="lazy"
/>
);
}
if (part.startsWith('[')) {
const match = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
if (!match) return <span key={`${part}-${index}`}>{part}</span>;
const [, label, href] = match;
return (
<a
key={`${part}-${index}`}
href={href}
target="_blank"
rel="noreferrer"
>
{label}
</a>
);
}
if (part.startsWith('**')) { if (part.startsWith('**')) {
return ( return (
<strong key={`${part}-${index}`}>{part.replace(/\*\*/g, '')}</strong> <strong key={`${part}-${index}`}>{part.replace(/\*\*/g, '')}</strong>
@@ -20,6 +53,13 @@ const renderInline = (text) => {
} }
return <span key={`${part}-${index}`}>{part}</span>; return <span key={`${part}-${index}`}>{part}</span>;
}); });
if (segmentIndex < segments.length - 1) {
rendered.push(<br key={`br-${segmentIndex}`} />);
}
return rendered;
});
}; };
const renderMarkdown = (body) => { const renderMarkdown = (body) => {
@@ -60,6 +100,18 @@ const renderMarkdown = (body) => {
return; return;
} }
if (/^---$/.test(line.trim())) {
flushList();
blocks.push({ type: 'hr' });
return;
}
if (/^>\s+/.test(line)) {
flushList();
blocks.push({ type: 'quote', value: line.replace(/^>\s+/, '') });
return;
}
if (/^[-*]\s+/.test(line)) { if (/^[-*]\s+/.test(line)) {
list.push(line.replace(/^[-*]\s+/, '')); list.push(line.replace(/^[-*]\s+/, ''));
return; return;
@@ -108,6 +160,13 @@ const renderMarkdown = (body) => {
<code>{block.value}</code> <code>{block.value}</code>
</pre> </pre>
); );
if (block.type === 'quote')
return (
<blockquote key={index} className="md-quote">
{renderInline(block.value)}
</blockquote>
);
if (block.type === 'hr') return <hr key={index} className="md-hr" />;
return ( return (
<p key={index} className="md-paragraph"> <p key={index} className="md-paragraph">
{renderInline(block.value)} {renderInline(block.value)}
@@ -118,8 +177,59 @@ const renderMarkdown = (body) => {
const Blog = () => { const Blog = () => {
const posts = useMemo(() => getBlogPosts(), []); const posts = useMemo(() => getBlogPosts(), []);
const [activeSlug, setActiveSlug] = useState(posts[0]?.slug); const categoryNames = ['일상', '개발', '공부', '아이디어'];
const activePost = posts.find((post) => post.slug === activeSlug) || posts[0]; const categorized = useMemo(() => {
const map = new Map(categoryNames.map((name) => [name, []]));
const misc = [];
posts.forEach((post) => {
const matched = categoryNames.find((name) => post.tags.includes(name));
if (matched) {
map.get(matched).push(post);
} else {
misc.push(post);
}
});
return {
categories: categoryNames.map((name) => ({
name,
items: map.get(name),
})),
misc,
};
}, [posts]);
const [selectedCategory, setSelectedCategory] = useState('전체');
const [page, setPage] = useState(1);
const pageSize = 10;
const filteredPosts = useMemo(() => {
if (selectedCategory === '전체') return posts;
if (selectedCategory === '기타') return categorized.misc;
return posts.filter((post) => post.tags.includes(selectedCategory));
}, [posts, categorized.misc, selectedCategory]);
const totalPages = Math.max(1, Math.ceil(filteredPosts.length / pageSize));
const pagedPosts = filteredPosts.slice((page - 1) * pageSize, page * pageSize);
const [activeSlug, setActiveSlug] = useState(pagedPosts[0]?.slug);
const activePost =
pagedPosts.find((post) => post.slug === activeSlug) || pagedPosts[0];
useEffect(() => {
if (page > totalPages) {
setPage(1);
}
}, [page, totalPages]);
useEffect(() => {
if (!pagedPosts.find((post) => post.slug === activeSlug)) {
setActiveSlug(pagedPosts[0]?.slug);
}
}, [pagedPosts, activeSlug]);
useEffect(() => {
setPage(1);
}, [selectedCategory]);
return ( return (
<div className="blog"> <div className="blog">
@@ -141,7 +251,21 @@ const Blog = () => {
<div className="blog-grid"> <div className="blog-grid">
<aside className="blog-list"> <aside className="blog-list">
{posts.map((post) => ( <div className="blog-category-filter">
{['전체', ...categoryNames, '기타'].map((name) => (
<button
key={name}
type="button"
className={`blog-category-chip${
selectedCategory === name ? ' is-active' : ''
}`}
onClick={() => setSelectedCategory(name)}
>
{name}
</button>
))}
</div>
{pagedPosts.map((post) => (
<button <button
key={post.slug} key={post.slug}
type="button" type="button"
@@ -155,6 +279,29 @@ const Blog = () => {
<span className="blog-list__meta">{post.date || '작성일 미정'}</span> <span className="blog-list__meta">{post.date || '작성일 미정'}</span>
</button> </button>
))} ))}
<div className="blog-pagination">
<button
type="button"
className="blog-page-btn"
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
disabled={page === 1}
>
이전
</button>
<span className="blog-page-indicator">
{page} / {totalPages}
</span>
<button
type="button"
className="blog-page-btn"
onClick={() =>
setPage((prev) => Math.min(totalPages, prev + 1))
}
disabled={page === totalPages}
>
다음
</button>
</div>
</aside> </aside>
<article className="blog-article"> <article className="blog-article">
{activePost ? ( {activePost ? (
@@ -183,6 +330,64 @@ const Blog = () => {
)} )}
</article> </article>
</div> </div>
<section className="blog-categories">
<div className="blog-categories__head">
<h2>카테고리</h2>
<p>태그 기준으로 글을 묶어 한눈에 확인할 있습니다.</p>
</div>
<div className="blog-categories__grid">
{categorized.categories.map((group) => (
<button
key={group.name}
type="button"
className="blog-category-card"
onClick={() => setSelectedCategory(group.name)}
>
<div className="blog-category-card__head">
<span>{group.name}</span>
<span className="blog-category-card__count">
{group.items.length}
</span>
</div>
<div className="blog-category-card__list">
{group.items.length ? (
group.items.slice(0, 3).map((post) => (
<span key={post.slug}>{post.title}</span>
))
) : (
<span className="blog-category-card__empty">
아직 글이 없습니다.
</span>
)}
</div>
</button>
))}
<button
type="button"
className="blog-category-card"
onClick={() => setSelectedCategory('기타')}
>
<div className="blog-category-card__head">
<span>기타</span>
<span className="blog-category-card__count">
{categorized.misc.length}
</span>
</div>
<div className="blog-category-card__list">
{categorized.misc.length ? (
categorized.misc.slice(0, 3).map((post) => (
<span key={post.slug}>{post.title}</span>
))
) : (
<span className="blog-category-card__empty">
아직 글이 없습니다.
</span>
)}
</div>
</button>
</div>
</section>
</div> </div>
); );
}; };