diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4db807 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# 빌드 산출물 +*.pdf diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ae5cdbd --- /dev/null +++ b/Makefile @@ -0,0 +1,19 @@ +TYPST := typst +FONTFLAGS := --font-path report/fonts +SRCS := $(wildcard report/day*.typ) +PDFS := $(SRCS:.typ=.pdf) +DEPS := report/연구실실습일지작성양식.typ report/hy-bg.png $(wildcard report/fonts/*) +MERGED := report/day_1-5.pdf + +all: $(MERGED) + +$(MERGED): $(PDFS) + pdfunite $(shell ls report/day*.pdf | sort -V) $@ + +report/day%.pdf: report/day%.typ $(DEPS) + $(TYPST) compile $(FONTFLAGS) $< $@ + +clean: + rm -f $(PDFS) + +.PHONY: all clean diff --git a/report/day1.typ b/report/day1.typ new file mode 100644 index 0000000..2dfbb4d --- /dev/null +++ b/report/day1.typ @@ -0,0 +1,49 @@ +#import "연구실실습일지작성양식.typ": 실습일지 + +#실습일지( + 소속대학: "공과대학", + 소속학과: "컴퓨터소프트웨어학부", + 학번: "2024062806", + 성명: "주하진", + 일시: "2026년 9월 4일 13시 부터 ~ 16시 까지", + 내용: [ + `Rust`로 문제를 해결하기 위해서 `rustup` 설치 및 환경 설정을 했습니다. + 그리고 가장 기초적인 완전 탐색 문제인 _최고의 33위치_를 해결했습니다. + + ```rust + let mut maxima = 0; + for i in 1..n - 1 { + for j in 1..n - 1 { + let mut s = 0; + for ii in 0..3 { + for jj in 0..3 { + s += grid[i + ii - 1][j + jj - 1]; + } + } + maxima = std::cmp::max(maxima, s);}} + ``` + + 해결법은 단순하게 3x3 윈도우의 합을 모든 가능한 위치에 적용하여 최대 합을 구하는 방식입니다. 매우 단순한 문제입니다. + + 다음으로 _행복한 수열의 개수_ 문제도 풀었는데, 그냥 모든 열과 행에 대해서 *행복한 수열의 조건*을 만족하는지, 확인하면 됩니다. $O(n^2)$으로 풀 수 있습니다. + + ```rust + for i in 0..n { + let mut prev = 101; + let mut succ = 1; + + for j in 0..n { + if grid[i][j] == prev { succ += 1; } else { + prev = grid[i][j]; + succ = 1; + } + if succ == m { cnt += 1; break; } + } + } + // ... + + ``` + + + ], +) diff --git a/report/day2.typ b/report/day2.typ new file mode 100644 index 0000000..0d2a322 --- /dev/null +++ b/report/day2.typ @@ -0,0 +1,42 @@ +#import "연구실실습일지작성양식.typ": 실습일지 + +#실습일지( + 소속대학: "공과대학", + 소속학과: "컴퓨터소프트웨어학부", + 학번: "2024062806", + 성명: "주하진", + 일시: "2026년 9월 11일 13시 부터 ~ 16시 까지", + 내용: [ + 지난 시간에 이어서 트로미노 문제를 해결했습니다. + + 이것도 완전탐색 문제로 33위치 문제랑 거의 비슷한 문제입니다. 다만 33위치 문제는 모양이 단일했지만, 트로미노 문제는 모양이 최대 6가지가 존재합니다. (ㄴ자를 회전한 경우와 ㅡ자를 회전한 경우) 이것을 효율적으로 구하기 위해 2x2 윈도우랑 3x1, 1x3 윈도우를 사용하여 해결했습니다. 2x2 윈도우에서 블럭 하나씩 총 4개의 모양을 확인하고 3x1, 1x3 윈도우를 모두 사용하면 전체 경우의 수를 완전 탐색할 수 있습니다. + + 금 채굴하기 문제는 조금 어려웠습니다. 다만 $n$이 매우 작고 ($< 100$) $k$를 0부터 max of $n$까지 잡아도 시간초과가 나지 않겠다 싶어서 대략 $O(n^4)$으로 풀었습니다. 아마 최적화를 한다면 $O(n^3)$까지 줄일 수 있다고 생각합니다. + + ```rust + for i in 0..n { + for j in 0..n { + for ii in 0..20 { + let s = drill(&field, n, ii, i, j); + if s * m >= cost(ii) { + maxima = maxima.max(s); + } + } + } + } + ``` + + ```rust + // in drill function + for i in 0..n { + for j in 0..n { + if (i as i64 - r as i64).abs() + (j as i64 - c as i64).abs() <= k { + s += field[i][j]; + } + } + } + + ``` + + ], +) diff --git a/report/day3.typ b/report/day3.typ new file mode 100644 index 0000000..5bf45eb --- /dev/null +++ b/report/day3.typ @@ -0,0 +1,41 @@ +#import "연구실실습일지작성양식.typ": 실습일지 + +#실습일지( + 소속대학: "공과대학", + 소속학과: "컴퓨터소프트웨어학부", + 학번: "2024062806", + 성명: "주하진", + 일시: "2026년 9월 18일 13시 부터 ~ 16시 까지", + 내용: [ + 오늘은 *단 한 번의 2048 시도* 문제를 풀었습니다. + 이 문제는 그냥 2048 게임의 슬라이드 동작을 구현하는 문제입니다. 다만, 그냥 일종의 시뮬레이션 문제이고, 격자의 크기도 매우 작고 (4), 단 한 번의 시도만 하면 되기 때문에 별다른 기교 없이 구현만 하면 됩니다. + + 그러면 각 방향에 slide 동작을 어떻게 구현할 것인가? 간단하게 생각을 해보면 모든 방향에 대해서 동작을 구현하면 됩니다. + + 각 방향에 대한 line들 각각에서 유효한 블럭들을 모아서 그 인접한 블럭을 조건에 맞게 합치고 그걸 업데이트하면 됩니다. + + 하지만, 위와 같은 방식은 중복되는 코드가 너무 많고 길어지기 때문에 가독성이 너무 안좋습니다. 단적으로는 예쁘지 않아서 더 나은 방식이 있습니다. 회전을 이용하면 하나의 코드로 모든 방향에 대한 slide 동작을 구현할 수 있습니다. + + 각 방향에 대한 올바른 rotate 수를 `rot`이라고 하면 + + ```rust + // where board: &mut [[usize; 4]; 4] + for _ in 0..rot { + rotate(board); + } + + for row in board.iter_mut() { + slide_merge(row); + } + + for _ in 0..(4 - rot) % 4 { + rotate(board); + } + ``` + + 깔끔하게 구협됩니다. + + + + ], +) diff --git a/report/day4.typ b/report/day4.typ new file mode 100644 index 0000000..b763447 --- /dev/null +++ b/report/day4.typ @@ -0,0 +1,36 @@ +#import "연구실실습일지작성양식.typ": 실습일지 + +#실습일지( + 소속대학: "공과대학", + 소속학과: "컴퓨터소프트웨어학부", + 학번: "2024062806", + 성명: "주하진", + 일시: "2026년 9월 25일 13시 부터 ~ 16시 까지", + 내용: [ + 이번에는 여러 공들이 벽안에서 움직이는 *벽이 있는 충돌 실험* 문제를 풀었습니다. + + 이 문제도 시뮬레이션 문제인데, 구슬이 초기 방향에 따라 초마다 움직이고, 구슬이 벽에 부딫이면 *반사*(1초가 걸려 반대 방향으로 바뀜)되고 구슬이 서로 부딫이면(같은 위치에 도달) *소멸*합니다. + + 이 조건에서 안정 상태(구슬의 개수가 더 이상 변하지 않는 상태)가 될 때까지 시뮬레이션을 진행하면 됩니다. + + 시뮬레이션은 대강 `field` 배열을 만들고 marble을 `(count, dir)`로 저장하고 (없으면 0) 매 스텝마다 구슬을 `new_field`에 업데이트하고 스텝이 끝나면 `count > 1`인 구슬을 제거합니다. 그리고 `field`를 `new_field`로 바꾸고 반복하면 됩니다. + + 이 문제를 풀때에는 좀 naive하게 대충 $t=4n$정도가 되면 안정 상태에 반드시 도달한다고 생각하고 푸니까 통과했습니다. + + 이후 생각을 해보니까 구슬은 철저하게 고정된 경로로 주기 $2n$에 따라 움직이므로 따라서 $2n$이면 충분히 안정 상태에 도달한다고 볼 수 있습니다. + + 이 점을 좀 더 자세하게 볼 수 있다. + + 먼저 우리가 쉽게 관찰 할 수 있는 사실은 다음과 같다. + 1. 다른 구슬의 방해가 없을 때 구슬은 주기성을 가진다. $bold(upright(S))_i (t) = bold(upright(S))_i (t + L)$ 그리고 이때 동일한 판에서 진행하는 구슬은 공통 주기 $L$을 가지며 $L=2n$이다. + 2. 소멸은 다른 생존 구슬의 궤적에 영향을 주지 않는다. + + 구슬 $A$와 $B$이 서로 겹치는 순간이 있다고 생각하자. 이때의 시각을 $t$라고 한다면, 두 구슬 모두 주기가 $L$이므로 $t mod L$에서도 같은 칸에 있다. 따라서 이 때 최초 충돌이 존재한다면 이때의 시간 $t^*_(A B)$는 $0 <= t^*_(A B) < L$이다. 다른 구슬 $C$가 있더라도 이 구슬이 둘 중 하나와 충돌 하더라도 다른 구슬에 영향을 주지 않으므로 최초 충돌 시간은 $0 <= t^* < L$을 만족한다. + + 따라서 모든 충돌은 $0 <= t < L$에 존재하며, 따라서 $L$이면 충분히 안정 상태에 도달한다고 볼 수 있다. + + + + 그래서 *4n*은 충분조건입니다. + ], +) diff --git a/report/day5.typ b/report/day5.typ new file mode 100644 index 0000000..9eb8742 --- /dev/null +++ b/report/day5.typ @@ -0,0 +1,46 @@ +#import "연구실실습일지작성양식.typ": 실습일지 + +#실습일지( + 소속대학: "공과대학", + 소속학과: "컴퓨터소프트웨어학부", + 학번: "2024062806", + 성명: "주하진", + 일시: "2026년 10월 2일 13시 부터 ~ 16시 까지", + 내용: [ + 이번엔 DFS를 풀어보고 싶어서 *뿌요뿌요*를 봤습니다. 하지만 이 문제는 DFS로도 풀 수 있지만, 실제로 Union-Find를 이용하는 것이 더 간단한 것으로 보여서 Union-Find를 이용해서 풀었습니다. + + Union-Find는 Disjoint Set을 관리하는 자료구조로 말 그대로 `union`과 `find`라는 두 가지 연산을 통해서 다른 집합을 합치거나 특정 원소가 속한 집합을 찾는 연산을 수행할 수 있습니다. + + 이 문제에서 뿌요뿌요 블럭의 연결된 블럭들을 Union-Find를 이용해서 관리하면, 각 블럭이 속한 집합의 크기를 쉽게 알 수 있고 쉽게 합칠 수 있습니다. + + 일단 `parents` 배열과 `counts` 배열을 만듭니다. + + ```rust + let n = field.len(); + let mut parents = (0..n * n).collect::>(); + let mut counts = vec![1; n * n]; + ``` + + 그리고 `union`과 `find`를 이용해서 연결부분을 찾습니다. 그리고 이 연결부분을 `union`합니다. 이때 `union`에서는 일반적인 Union-Find와 다르게 `counts`와 관련된 특수한 처리를 해줘야 합니다. 왜냐하면 `union`을 할 때, 두 집합의 크기를 합쳐야 하기 때문입니다. + + ```rust + let current = field[i][j]; // foreach i, j + let index = i * n + j; // because of 1D representation + if i > 0 && field[i - 1][j] == current { + union(&mut parents, &mut counts, index, (i - 1) * n + j); + } + if j > 0 && field[i][j - 1] == current { + union(&mut parents, &mut counts, index, i * n + (j - 1)); + } + ``` + 이제 모든 components를 찾고 blastable한 컴포넌트를 찾고 그 개수를 새로 그중 `counts`가 가장 큰 컴포넌트를 찾으면 됩니다. + ```rust + let comps = (0..n * n).map(|i| find(&mut parents, i)).collect::>(); + let count = comps.iter().filter(|&&x| counts[x] >= 4).count(); + let maxima = comps.iter().map(|&x| counts[x]).max().unwrap_or(0); + (count, maxima) + ``` + + + ], +) diff --git a/report/fonts/ChosunKm.ttf b/report/fonts/ChosunKm.ttf new file mode 100644 index 0000000..87c14d6 Binary files /dev/null and b/report/fonts/ChosunKm.ttf differ diff --git a/report/fonts/ChosunSm.ttf b/report/fonts/ChosunSm.ttf new file mode 100644 index 0000000..996df70 Binary files /dev/null and b/report/fonts/ChosunSm.ttf differ diff --git a/report/hy-bg.png b/report/hy-bg.png new file mode 100644 index 0000000..20be4e9 Binary files /dev/null and b/report/hy-bg.png differ diff --git a/report/연구실실습일지작성양식.typ b/report/연구실실습일지작성양식.typ new file mode 100644 index 0000000..b6a2ecc --- /dev/null +++ b/report/연구실실습일지작성양식.typ @@ -0,0 +1,199 @@ +// 연구실 실습일지 양식 (원본 docx 1페이지 복제 + 템플릿 함수) +// 원본: 연구실실습일지작성양식_컴퓨터소프트웨어실습HY-UP(실용연구실습)2_(1-15회차).docx +// +// 이 파일을 직접 컴파일하면 빈 양식이 나오고, +// #import "연구실실습일지작성양식.typ": 실습일지 +// #실습일지(소속대학: "…", 일시: "…", 내용: […]) +// 처럼 호출하면 채워진 실습일지 1페이지가 나온다 (예: day1.typ). +// +// 모든 요소는 절대 좌표(place) 배치 — 글꼴에 따라 줄이 바뀌지 않는다. +// 수치는 docx의 twips/EMU → pt 환산값: +// 페이지 595×841pt / 배경 이미지 (0, 30.8) 594.88×810.2 +// 제목 상자 (45.35, 73.7) 504.35×42.6, 테두리 1.44pt +// 표: (46.1, 143.15) 폭 505.75, 높이 630.15 (4행 높이 34.6pt는 원본 그대로, +// 원본에선 '3시간'이 2줄로 접혀 늘어난 높이지만 렌더는 1줄로 한다) + +// kr-font: report/fonts/ChosunSm.ttf = 조선신명조 풀 폰트(17,147 글리프). +// typst는 패밀리를 Mac 플랫폼(1/0) name 레코드로만 매칭하는데 원본은 이 레코드가 +// 빈 문자열이라(Windows 레코드의 '조선신명조'는 무시됨) 폰트에 'ChosunSm'으로 기입해 둠. +// Regular face만 존재 — 굵게/기울임은 아래 show strong/emph에서 처리. +#let kr-font = ("ChosunSm", "NanumGothic") + +// 코드 폰트: 한글 지원 모노스페이스 우선(주석 한글도 모노폭), 없는 글리프는 kr-font로 폴백 +#let code-font = ("NanumGothicCoding", "DejaVu Sans Mono", "Liberation Mono") + +#let X0 = 46.1pt // 표 왼쪽 (좌 여백 37 + 표 들여쓰기 9.1) +#let Y0 = 143.15pt // 표 위쪽 (외곽 테두리 기준) +#let BX = (0pt, 100.75pt, 264.35pt, 339.9pt, 362.45pt, 419.05pt, 433.2pt, 505.75pt) // 열 경계 +#let RY = (0pt, 23.05pt, 43.25pt, 63.55pt, 98.15pt, 121.7pt, 533.9pt, 557.6pt, 630.15pt) // 행 경계 + +// 굵은 라벨 / 값 (10pt). trk = Word 자간(spc, twips→pt) +#let lab(body, trk: 0pt) = text(font: kr-font, size: 10pt, weight: "bold", tracking: trk, body) +#let val(body, trk: 0pt) = text(font: kr-font, size: 10pt, tracking: trk, body) + +// 셀 안내선: 가는 내부선 0.5pt (Word sz=4), 굵은 선 1.5pt (Word sz=12) +#let hline(i, j, y, w) = place(dx: X0 + BX.at(i), dy: Y0 + y, line(length: BX.at(j) - BX.at(i), stroke: w * 1pt + black)) +#let vline(x, y0, y1, w) = place(dx: X0 + x, dy: Y0 + y0, line(angle: 90deg, length: y1 - y0, stroke: w * 1pt + black)) + +// 셀 텍스트: (x0, x1) = 셀 좌우, r = 행 번호. 셀 정중앙 배치 +#let cell-text(x0, x1, r, body, li: 5.4pt, ri: 5.4pt) = place( + dx: X0 + x0, dy: Y0 + RY.at(r), + box(width: x1 - x0, height: RY.at(r + 1) - RY.at(r), + inset: (top: 0pt, bottom: 0pt, left: li, right: ri), + align(center + horizon, body))) + +#let cell-left(x0, x1, r, body, li: 5.4pt, ri: 5.4pt) = place( + dx: X0 + x0, dy: Y0 + RY.at(r), + box(width: x1 - x0, height: RY.at(r + 1) - RY.at(r), + inset: (top: 0pt, bottom: 0pt, left: li, right: ri), + align(left + horizon, body))) + +// 날짜 칸의 탭 정지 토큰 (원본 탭: 1210/1810/2511/3909 twips, 문단 들여쓰기 611 기준 상대 위치) +// 오프셋 기준점 = 셀 왼쪽 + 셀 여백 5.4 + 문단 들여쓰기 30.55 = +35.95 +#let date-token(r, x-offset, body) = place( + dx: X0 + 100.75pt + 35.95pt + x-offset, dy: Y0 + RY.at(r), + box(height: RY.at(r + 1) - RY.at(r), inset: (top: 0pt, bottom: 0pt, left: 0pt, right: 0pt), + align(horizon, body))) + +// ── 템플릿 함수 ──────────────────────────────────────────────────── +// 소속대학/소속학과/학번/성명/의견: 문자열(빈 값이면 원본처럼 빈 칸) +// 일시: none이면 빈 양식의 '년 월 일 시 부터 ~ 시 까지' 스캐폴드, 문자열이면 그대로 출력 +// 내용: none이면 빈 칸, content/문자열이면 실습 내용 칸(왼쪽 위)에 출력 +#let 실습일지(소속대학: "", 소속학과: "", 학번: "", 성명: "", 일시: none, 내용: none, 의견: "") = { + set page(width: 595pt, height: 841pt, margin: 0pt) + set par(spacing: 0pt) + set text(lang: "ko", cjk-latin-spacing: none) + + // 코드 블록 스타일 (내용 칸의 ``` 코드): 둥근 박스 + 연한 배경 + 테두리 + show raw.where(block: true): it => block( + width: 100%, + above: 6pt, + below: 6pt, + fill: rgb("#F6F8FA"), + stroke: 0.5pt + rgb("#C9D1D9"), + radius: 3pt, + inset: (x: 8pt, y: 6pt), + text(font: code-font + kr-font, size: 8.5pt, it)) + // 인라인 코드: 옅은 배경만 + show raw.where(block: false): it => box( + fill: rgb("#EEF1F4"), + inset: (x: 3pt, y: 0pt), + radius: 2pt, + text(font: code-font + kr-font, size: 9pt, it)) + + // *굵게*: 조선신명조엔 Bold face가 없으므로 같은 조선일보 서체 계열인 + // 조선굵은명조(report/fonts/ChosunKm.ttf, name 테이블 동일 패치)로 렌더한다 + show strong: it => text(font: ("ChosunKm",) + kr-font, it) + // box로 감싸지 않으면 skew가 문단을 분리해 par(spacing: 0pt) 하에서 줄이 겹친다 + show emph: it => box(skew(ax: -12deg, text(font: ("Liberation Serif",) + kr-font, it))) + [ + // 배경 (한양대 양식 이미지) + #place(dx: 0pt, dy: 30.8pt, image("hy-bg.png", width: 594.88pt, height: 810.2pt)) + + // 제목 상자 + #place(dx: 45.35pt, dy: 73.7pt, + rect(width: 504.35pt, height: 42.6pt, stroke: 1.44pt + black, fill: none, inset: 0pt, + align(center + horizon, + text(font: kr-font, size: 25pt, weight: "bold")[연구실] + + text(font: kr-font, size: 25pt, weight: "bold", tracking: 3pt)[ ] + + text(font: kr-font, size: 25pt, weight: "bold", tracking: -0.2pt)[실습일지]))) + + // 표 테두리: 가로선 (바깥 1.5pt, 내부 0.5pt — 학번↔교과목명 경계만 1.5pt) + #hline(0, 7, 0pt, 1.5) + #hline(0, 7, 23.05pt, 0.5) + #hline(0, 7, 43.25pt, 1.5) + #hline(0, 7, 63.55pt, 0.5) + #hline(0, 7, 98.15pt, 0.5) + #hline(0, 7, 121.7pt, 0.5) + #hline(0, 7, 533.9pt, 0.5) + #hline(0, 7, 557.6pt, 0.5) + #hline(0, 7, 630.15pt, 1.5) + // 세로선: 셀 경계가 실제로 존재하는 구간만 + #vline(0pt, 0pt, 630.15pt, 1.5) // 왼쪽 외곽 + #vline(505.75pt, 0pt, 630.15pt, 1.5) // 오른쪽 외곽 + #vline(100.75pt, 0pt, 630.15pt, 0.5) // 라벨 열 오른쪽 (전 행) + #vline(264.35pt, 0pt, 43.25pt, 0.5) // 소속대학 값 | 소속학과 (1–2행) + #vline(339.9pt, 0pt, 98.15pt, 0.5) // 소속학과 값 | 수업번호 열 (1–4행) + #vline(362.45pt, 557.6pt, 630.15pt, 0.5) // 의견 칸 | 담당 교수 확인 (8행) + #vline(419.05pt, 43.25pt, 98.15pt, 0.5) // 수업번호 칸 | 11725 칸 (3–4행) + #vline(433.2pt, 557.6pt, 630.15pt, 0.5) // 담당 교수 확인 | (인) (8행) + + // 1행: 소속대학 / 소속학과 + #cell-text(0pt, 100.75pt, 0, lab(trk: -0.2pt)[소속대학]) + #cell-text(264.35pt, 339.9pt, 0, lab(trk: -0.2pt)[소속학과]) + #if 소속대학 != "" [#cell-left(100.75pt, 264.35pt, 0, val(소속대학))] + #if 소속학과 != "" [#cell-left(339.9pt, 505.75pt, 0, val(소속학과))] + + // 2행: 학번 / 성명 + #cell-text(0pt, 100.75pt, 1, lab(trk: -0.25pt)[학번]) + #cell-text(264.35pt, 339.9pt, 1, lab(trk: -0.25pt)[성명]) + #if 학번 != "" [#cell-left(100.75pt, 264.35pt, 1, val(학번))] + #if 성명 != "" [#cell-left(339.9pt, 505.75pt, 1, val(성명))] + + // 3행: 교과목명 / 컴퓨터소프트웨어실습… / 수업번호 / 11725 + #cell-text(0pt, 100.75pt, 2, lab(trk: -0.2pt)[교과목명]) + #cell-left(100.75pt, 339.9pt, 2, + val(trk: -0.1pt)[컴퓨터소프트웨어실습HY-UP(실용연구실습)2], li: 11pt) + #cell-text(339.9pt, 419.05pt, 2, lab(trk: -0.2pt)[수업번호]) + #cell-left(419.05pt, 505.75pt, 2, val(trk: -0.1pt)[11725], li: 10.4pt) + + // 4행: 실습 일시 / 실습진행시간 / 3시간(1줄 — 높이만 원본 그대로 34.6pt) + #cell-text(0pt, 100.75pt, 3, lab[실습] + lab(trk: -0.1pt)[ ] + lab(trk: -0.25pt)[일시]) + #if 일시 == none [ + // 빈 양식: 원본 탭 정지 스캐폴드 + #date-token(3, 0pt, val(trk: -0.5pt)[년]) + #date-token(3, 29.95pt, val[월]) + #date-token(3, 59.95pt, val[일]) + #date-token(3, 95pt, val[시] + val(trk: -0.15pt)[ ] + val[부터] + val(trk: -0.15pt)[ ] + val[\~]) + #date-token(3, 164.9pt, val[시] + val(trk: -0.1pt)[ ] + val(trk: -0.25pt)[까지]) + ] else [ + #cell-left(100.75pt, 339.9pt, 3, val(일시), li: 35.95pt) + ] + #cell-text(339.9pt, 419.05pt, 3, lab(trk: -0.1pt)[실습진행시간]) + #cell-text(419.05pt, 505.75pt, 3, val(trk: -0.25pt)[3시간]) + + // 5행: 실습 장소 + #cell-text(0pt, 100.75pt, 4, lab[실습] + lab(trk: -0.1pt)[ ] + lab(trk: -0.25pt)[장소]) + #cell-left(100.75pt, 505.75pt, 4, + val[IT/BT] + val[관] + val(trk: -0.45pt)[ ] + val[811호와] + val(trk: -0.3pt)[ ] + val(trk: -0.2pt)[코드트리], + li: 10.5pt) + + // 6행: 실습 내용 (라벨은 Word vAlign=center — 세로 중앙) + 기재 내용(왼쪽 위) + #cell-text(0pt, 100.75pt, 5, lab[실습] + lab(trk: -0.1pt)[ ] + lab(trk: -0.25pt)[내용]) + #if 내용 != none [ + #place(dx: X0 + 100.75pt, dy: Y0 + RY.at(5), + box(width: 405pt, height: 412.2pt, + inset: (top: 5.4pt, bottom: 5.4pt, left: 5.4pt, right: 5.4pt), + align(top + left, { + // 전역 par(spacing: 0pt)를 상속하면 문단/목록 사이 간격이 사라지므로 내용 칸에서만 복구 + set par(spacing: 0.8em) + text(font: kr-font, size: 10pt, 내용) + }))) + ] + + // 7행: 담당 교수명 / 박희진 + #cell-text(0pt, 100.75pt, 6, lab[담당] + lab(trk: -0.2pt)[ ] + lab(trk: -0.25pt)[교수명]) + #cell-left(100.75pt, 505.75pt, 6, val(trk: -0.25pt)[박희진], li: 10.5pt) + + // 8행: 담당교수 의견 / 담당 교수 확인 / (인) + #cell-text(0pt, 100.75pt, 7, lab[담당교수] + lab(trk: -0.3pt)[ ] + lab(trk: -0.25pt)[의견]) + #if 의견 != "" [#cell-left(100.75pt, 362.45pt, 7, val(의견))] + // 담당 교수 확인: 원본 들여쓰기 때문에 '담당 교/수 확인'으로 접히는 양식 결함을 + // '담당 교수' / '확인' 2줄 중앙 정렬로 명시 배치 + #cell-text(362.45pt, 433.2pt, 7, + lab[담당] + lab(trk: -1.25pt)[ ] + lab[교수] + linebreak() + lab(trk: -0.3pt)[확인]) + #cell-text(433.2pt, 505.75pt, 7, lab(trk: -0.25pt)[(인)]) + + // 빨간 안내문 + #let r(body, trk: 0pt) = text(font: kr-font, size: 11pt, weight: "bold", fill: red, tracking: trk, body) + #place(dx: 42.45pt, dy: 774pt, + r[※] + r(trk: -0.1pt)[ ] + r[1회차에] + r(trk: -0.05pt)[ ] + r[3시간,] + r(trk: 0.1pt)[ ] + + r[15회차] + r(trk: -0.05pt)[ ] + r[이상의] + r(trk: 0.1pt)[ ] + r[연구실] + r(trk: -0.05pt)[ ] + + r[실습일지] + r(trk: 0.1pt)[ ] + r[작성] + r(trk: 0.1pt)[ ] + r[필수] + r(trk: 0.1pt)[ ] + + r[(] + r(trk: -0.05pt)[ ] + r[한학기] + r(trk: -0.05pt)[ ] + r[45시간] + r(trk: -0.05pt)[ ] + + r[실습] + r(trk: -0.05pt)[ ] + r[필수] + r(trk: -0.05pt)[ ] + r(trk: -0.5pt)[)]) + ] +} + +// 이 파일을 직접 컴파일하면 빈 양식 +#실습일지()