Bootstrap의 Card(카드)에 이미지와 게시물 내용 말줄임 처리로 UI를 변경하도록 하겠습니다.
이전 Node.js 레스트 API 서버에는 초기 데이터를 블로그처럼 수정하였습니다. 그리고 게시물에 대표 이미지와 카테고리, 조회 수가 추가되었습니다.
현재 Card(카드) UI에 대표 이미지와 게시물 내용을 추가하겠습니다.
1. C:\workspaces\nodeserver\testvue\src\views\Home.vue 파일을 오픈하여 Card(카드)에 카드 이미지(card-img-top)를 추가합니다. 카드 이미지는 카드 보디(card-body) 위에 추가합니다.
<div class="py-5">
<div class="container text-start">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
<div class="col" v-for="boardItem in boardList" v-bind:key="boardItem.no">
<div class="card">
<img class="card-img-top" :src="(boardItem.poster.toUpperCase().startsWith('HTTP') ? '' : 'http://localhost:9000') + boardItem.poster" alt="">
<div class="card-body">
<h5 class="card-title">{{boardItem.subject}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{boardItem.writer}}</h6>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" @click="boardNoClick(boardItem)">보기</button>
</div>
<small class="text-muted">{{boardItem.writedate}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
카드 이미지(card-img-top)의 src에 데이터 바인딩을 할 때는 이중 중괄호(일명 Mustache - 수염)는 사용하지 않아야 합니다. 그리고 이미지 경로를 Node.js 레스트 API 서버 경로로 처리해야 이미지가 나타납니다. 만약, 이미지 경로에 URL이 있으면 Node.js 레스트 API 서버 경로는 추가하지 말아야 합니다.
Card(카드)에 이미지가 적용되었습니다. 그런데 이미지의 크기가 각각 다르기 때문에 동일한 크기로 조정이 필요합니다.
카드 이미지의 크기를 동일하게 조정하기 위해서 <style>에 css를 추가합니다.
.card-img-top {
height: 15em;
object-fit: cover;
}
반응형 웹을 위해 카드 이미지의 높이를 절대 단위인 px 대신 상대 단위인 em를 사용하였습니다.
CSS에서 em은 요소의 font-size 속성 값에 비례해서 결정되는 상대 단위입니다.
요소의 font-size가 16px 이면 상대 단위인 em은 1em = 16px x 1 = 16px입니다. 그래서 15em 이면 240px(16px x 15)입니다.
참고로 rem은 최상위 요소의 font-size 속성 값에 비례합니다.
카드 이미지의 크기가 동일하게 조정되었습니다.
그리고 object-fit는 이미지의 크기(넓이, 높이)를 부모 요소에서 어떤 비율로 채울지 설정합니다.
cover은 부모 요소의 크기에 맞게 이미지의 크기를 비율대로 조정하여 공백 없이 나오도록 설정됩니다. 이미지를 부모 요소 안에서 중앙에 위치하게 합니다. 부모 요소보다 크기가 크면 부모 요소의 크기만큼만 나오고 크기가 작으면 확대되어 나옵니다.
fill은 부모 요소의 크기에 맞게 이미지의 크기가 맞추어 설정됩니다. 그래서 이미지가 비율대로 조정되지 않고 강제로 넓이와 높이가 변경되어서 찌그러지거나 퍼지게 됩니다.
.card-img-top {
height: 15em;
object-fit: fill;
}
contain은 부모 요소의 크기에 맞게 이미지의 크기가 비율대로 조정되어 설정됩니다. 이미지의 전체 화면이 부모 요소 안에 들어가게 됩니다. 크기가 작으면 확대되어 처리됩니다. (scale-up)
.card-img-top {
height: 15em;
object-fit: contain;
}
none은 이미지의 크기와 상관없이 부모 요소 안에서 중앙에 위치하게 설정됩니다.
.card-img-top {
height: 15em;
object-fit: none;
}
scale-down은 부모 요소의 크기에 맞게 이미지의 크기가 비율대로 조정되어 설정됩니다. 크기가 크면 작게 축소합니다.
.card-img-top {
height: 15em;
object-fit: scale-down;
}
2. Card(카드)에 카드 텍스트(card-text)를 추가합니다. 카드 텍스트(card-text)는 카드 타이틀(card-title) 아래에 추가합니다.
<div class="py-5">
<div class="container text-start">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
<div class="col" v-for="boardItem in boardList" v-bind:key="boardItem.no">
<div class="card">
<img class="card-img-top" :src="(boardItem.poster.toUpperCase().startsWith('HTTP') ? '' : 'http://localhost:9000') + boardItem.poster" alt="">
<div class="card-body">
<h5 class="card-title">{{boardItem.subject}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{boardItem.writer}}</h6>
<p class="card-text">{{boardItem.content}}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" @click="boardNoClick(boardItem)">보기</button>
</div>
<small class="text-muted">{{boardItem.writedate}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
카드 타이틀(card-title)이 길 경우 두 줄로 이상 나오게 되고 카드 텍스트(card-text) 내용이 적을 경우에 다른 카드보다 높이가 작게 됩니다. 그래서 카드 타이틀(card-title)과 카드 텍스트(card-text)에 높이와 말줄임 처리를 해야 합니다.
말줄임(...) 표시하기
말줄임은 여러 줄로 나오는 문자열을 한 줄로 처리하고 요서의 크기보다 크면 문자열 끝에 말줄임(...) 표시를 추가해 줍니다.
.card-title {
overflow: hidden;
text-overflow:ellipsis;
white-space: nowrap;
}
white-space를 nowrap 하여 자동 줄 바꿈을 하지 않게 되고 overflow를 hidden 처리하여 요소의 크기를 넘어갈 경우 보이지 않게 되고 text-overflow를 ellipsis로 생략 처리됩니다.
그런데 한 줄이 아닌 여러 줄로 표시하면서 말줄임 표시를 하고 싶은 경우 어떻게 해야 할까요?
webkit-box를 사용하면 됩니다.
.card-text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
-webkit-line-clamp은 display 속성이 -webkit-box이고 -webkit-box-orient 속성이 vertical로 설정한 경우에만 동작합니다.
-webkit-line-clamp은 블록 컨테이너의 콘텐츠를 지정한 줄 수만큼 제한합니다. 단 실제 줄 수가 지정한 줄 수보다 작으면 적용되지 않습니다.
<style>에서 css의 webkit-box를 사용하여 카드 타이틀(card-title)과 카드 텍스트(card-text)의 css를 추가합니다.
.card-title {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-text {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
그렇지만 webkit-box를 사용하여 처리해도 카드 타이틀(card-title)이 한 줄이거나 카드 텍스트(card-text)의 3줄 이하이면 적용되지 않습니다. 그래서 높이를 지정해 줘야 합니다.
높이(height)는 라인의 높이(line-height) x 줄 수로 계산하면 됩니다.
라인의 높이(line-height)가 1.4em이고 -webkit-line-clamp이 2이면 높이(height)는 2.8em으로 하면 됩니다.
<style>에서 카드 타이틀(card-title)과 카드 텍스트(card-text)에 높이를 추가합니다.
.card-title {
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4em;
height: 2.8em;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.card-text {
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.4em;
height: 4.2em;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
3. Card(카드)에 마진(margin)이 Card(카드)가 붙어 있습니다. 그래서 Card(카드)를 감싸고 있는 그리드 열(col)에 "my-2"를 추가하여 상하 마진을 줍니다.
<div class="py-5">
<div class="container text-start">
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
<div class="col my-2" v-for="boardItem in boardList" v-bind:key="boardItem.no">
<div class="card">
<img class="card-img-top" :src="(boardItem.poster.toUpperCase().startsWith('HTTP') ? '' : 'http://localhost:9000') + boardItem.poster" alt="">
<div class="card-body">
<h5 class="card-title">{{boardItem.subject}}</h5>
<h6 class="card-subtitle mb-2 text-muted">{{boardItem.writer}}</h6>
<p class="card-text">{{boardItem.content}}</p>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" @click="boardNoClick(boardItem)">보기</button>
</div>
<small class="text-muted">{{boardItem.writedate}}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>