fix: 기본기 전수 감사(38 에이전트, 172파일)에서 확정된 버그 10건 수정

[major]
- client.js 401 인터셉터: /auth/** 전체를 예외로 — 비밀번호 변경에서 현재 비번
  틀리면 세션 만료로 오인해 /login으로 튕기던 버그 (에러 안내 도달 불가였음)
- Submission (assignment_id,user_id) 유니크 제약 추가(엔티티+운영DB) —
  동시 제출 시 중복 행 → 이후 해당 과제 재제출 영구 500 방지. 자매 엔티티와 일관화

[minor]
- AuthContext: 마운트 시 /auth/me의 늦은 401이 로그인 성공을 덮는 경쟁 → 함수형 업데이트로 보존
- AssignmentsPage: key에 제출 id 포함 → 첫 제출 직후 카드 리마운트로 성공 메시지 소실
  → key를 과제 id로 고정, 메시지는 접히는 영역 밖으로
- CodeEditor: 텍스트 선택 후 Tab 시 커서가 선택 길이만큼 밀림 (selectionEnd+2 → selectionStart+2)
- div.olist>ol 마크업 오류로 목록 번호 이중 표시 — 7개 강좌 10곳 전부 ol.olist로 통일
  (감사가 3파일 발견, 전역 grep으로 4파일 추가 확보)
- SignupPage 검증을 서버 규칙과 일치: 아이디 4자+영숫자, 생년월일 과거만
- LoginRequest @NotBlank + @Valid: username 누락 시 NPE 500 → 400
- server.error.include-message: always — 서버의 한국어 에러 사유가 화면까지 전달
  ('이미 사용 중인 아이디입니다' 등, 그동안 폴백 문구만 보였음)

검증: 테스트 22개 통과, 운영에서 400/409 message/비번변경 에러표시 확인.
각 버그의 원리를 소스 주석에 학습 포인트로 기록.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
AWESOMEDEV 2026-07-17 11:42:05 +09:00
parent 03fdde7495
commit 0336213ca2
16 changed files with 85 additions and 49 deletions

View File

@ -11,7 +11,14 @@ import java.time.Instant;
* 멘토가 피드백을 남기면 status가 SUBMITTED REVIEWED로 바뀐다. * 멘토가 피드백을 남기면 status가 SUBMITTED REVIEWED로 바뀐다.
*/ */
@Entity @Entity
@Table(name = "submissions") // 학습 포인트: "학생당 과제당 제출물 1개" 규칙의 최종 방어선은 DB 유니크 제약이다.
// 서비스의 '있으면 갱신, 없으면 생성' 검사(check-then-insert) 거의 동시에
// 요청( 탭에서 동시 제출 ) "없네? 생성!" 통과할 있다.
// 그러면 중복 행이 생기고, 이후 findByAssignmentAndUser(Optional 반환) 2건을 만나
// 과제의 재제출이 전부 500으로 죽는다. 제약이 있으면 최악의 경우에도 DB가 한쪽을 거절한다.
// (CodingSubmission·Progress·CourseProgress도 같은 이유로 전부 제약을 갖고 있다.)
@Table(name = "submissions",
uniqueConstraints = @UniqueConstraint(columnNames = {"assignment_id", "user_id"}))
public class Submission { public class Submission {
@Id @Id

View File

@ -55,7 +55,7 @@ public class AuthController {
/** POST /api/auth/login — 로그인. 성공 시 세션이 만들어지고 사용자 정보를 돌려준다. */ /** POST /api/auth/login — 로그인. 성공 시 세션이 만들어지고 사용자 정보를 돌려준다. */
@PostMapping("/login") @PostMapping("/login")
public UserResponse login(@RequestBody LoginRequest loginRequest, public UserResponse login(@Valid @RequestBody LoginRequest loginRequest,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response) { HttpServletResponse response) {
// 0) 무차별 대입 방어 비밀번호를 검증하기 "전에" 잠금부터 확인한다. // 0) 무차별 대입 방어 비밀번호를 검증하기 "전에" 잠금부터 확인한다.

View File

@ -1,11 +1,21 @@
package dev.awesomedev.mirim.web.dto; package dev.awesomedev.mirim.web.dto;
import jakarta.validation.constraints.NotBlank;
/** /**
* 파일이 하는 : * 파일이 하는 :
* 로그인 요청 본문 {username, password} 담는 DTO다. * 로그인 요청 본문 {username, password} 담는 DTO다.
* *
* 학습 포인트: record는 필드·생성자·getter를 자동으로 만들어 주는 자바 문법으로, * 학습 포인트: record는 필드·생성자·getter를 자동으로 만들어 주는 자바 문법으로,
* "데이터를 담기만 하는 그릇" DTO에 맞는다. * "데이터를 담기만 하는 그릇" DTO에 맞는다.
*
* @NotBlank가 없던 시절의 버그: {"password":"x"}처럼 username 없이 요청하면
* 컨트롤러 줄의 username.toLowerCase()에서 NullPointerException 500 서버 오류.
* 사용자 입력은 "당연히 있겠지" 통하지 않는다 없을 있는 모든 것을 검증한다.
* (@Valid + @NotBlank 조합이면 null· 문자열 모두 400으로 정중히 거절된다.)
*/ */
public record LoginRequest(String username, String password) { public record LoginRequest(
@NotBlank(message = "아이디를 입력해 주세요.") String username,
@NotBlank(message = "비밀번호를 입력해 주세요.") String password
) {
} }

View File

@ -27,6 +27,14 @@ spring:
server: server:
port: 8080 port: 8080
error:
# 학습 포인트: 스프링 기본값은 에러 응답에 message를 "안 싣는" 것이다(never).
# 우리는 ResponseStatusException에 한국어 사유("이미 사용 중인 아이디입니다" 등)를
# 정성껏 담는데, 이 설정이 없으면 그 문구가 클라이언트에 전달되지 않아
# 프론트의 err.response.data?.message가 항상 undefined였다(사용자는 두루뭉술한
# 폴백 문구만 봄). always로 켜서 서버가 쓴 안내가 화면까지 닿게 한다.
# 주의: 예외 메시지가 노출되므로, 예외에는 내부 정보(SQL, 경로 등)를 담지 말 것.
include-message: always
servlet: servlet:
session: session:
timeout: 8h # 하루 근무 동안 로그인 유지 timeout: 8h # 하루 근무 동안 로그인 유지

View File

@ -21,10 +21,17 @@ export function AuthProvider({ children }) {
useEffect(() => { useEffect(() => {
// . // .
//
// (race condition) .
// ( + ).
// /auth/me 401 setUser(null) ,
// .
// " ":
// prev (=login() ) , .
client client
.get('/auth/me') .get('/auth/me')
.then((res) => setUser(res.data)) .then((res) => setUser((prev) => prev ?? res.data))
.catch(() => setUser(null)) // 401 .catch(() => setUser((prev) => prev)) // 401 = . ,
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, []); }, []);

View File

@ -21,11 +21,15 @@ client.interceptors.response.use(
(error) => { (error) => {
const status = error.response?.status; const status = error.response?.status;
const url = error.config?.url || ''; const url = error.config?.url || '';
// 예외: 로그인 시도 자체의 401(비밀번호 틀림)과 // 예외: /auth/** 의 401은 "세션 만료"가 아니라 그 기능 자체의 결과다 —
// 로그인 여부 확인용 /auth/me의 401은 리다이렉트하면 안 된다. // 로그인 실패(비밀번호 틀림), /auth/me(비로그인 확인),
// (로그인 페이지에서 무한 새로고침이 되어버린다.) // 비밀번호 변경(현재 비밀번호 틀림) 모두 각 페이지가 직접 안내한다.
const isAuthCheck = url.includes('/auth/login') || url.includes('/auth/me'); // ⚠️ 실제로 났던 버그: 처음엔 login/me만 예외로 뒀더니, 비밀번호 변경에서
if (status === 401 && !isAuthCheck && window.location.pathname !== '/login') { // 현재 비밀번호를 틀리면 이 인터셉터가 "세션 만료"로 오인해 /login으로 튕겨 버려
// 사용자가 에러 안내를 영영 못 봤다. 인터셉터처럼 "전역으로 가로채는" 코드는
// 예외 목록이 좁으면 멀쩡한 기능을 잡아먹는다 — 의미 단위(/auth/ 전체)로 묶는 이유.
const isAuthEndpoint = url.includes('/auth/');
if (status === 401 && !isAuthEndpoint && window.location.pathname !== '/login') {
window.location.href = '/login'; window.location.href = '/login';
} }
return Promise.reject(error); return Promise.reject(error);

View File

@ -34,9 +34,13 @@ export default function CodeEditor({ value, onChange, readOnly = false, minRows
const next = value.slice(0, selectionStart) + ' ' + value.slice(selectionEnd); const next = value.slice(0, selectionStart) + ' ' + value.slice(selectionEnd);
onChange(next); onChange(next);
// . // .
// " 2 " = selectionStart + 2.
// selectionEnd + 2 Tab
// , .
// ( start == end .)
requestAnimationFrame(() => { requestAnimationFrame(() => {
textarea.selectionStart = selectionEnd + 2; textarea.selectionStart = selectionStart + 2;
textarea.selectionEnd = selectionEnd + 2; textarea.selectionEnd = selectionStart + 2;
}); });
} }

View File

@ -89,9 +89,6 @@ function AssignmentCard({ assignment, submission, onSubmitted }) {
/> />
</div> </div>
{error && <p className="error-text">{error}</p>} {error && <p className="error-text">{error}</p>}
{savedMessage && (
<p style={{ color: 'var(--teal)', fontSize: 14, marginBottom: 8 }}>{savedMessage}</p>
)}
<button className="btn btn-primary" type="submit" disabled={saving}> <button className="btn btn-primary" type="submit" disabled={saving}>
{saving ? '제출 중...' : submission ? '다시 제출하기' : '제출하기'} {saving ? '제출 중...' : submission ? '다시 제출하기' : '제출하기'}
</button> </button>
@ -102,6 +99,14 @@ function AssignmentCard({ assignment, submission, onSubmitted }) {
)} )}
</form> </form>
</details> </details>
{/* details "" (open={!submission})
메시지는 계속 보여야 하니까. 접히는 영역 안에 두면 성공하자마자 같이 숨어 버린다. */}
{savedMessage && (
<p style={{ color: 'var(--teal)', fontSize: 14, marginTop: 10, marginBottom: 0 }}>
{savedMessage}
</p>
)}
</div> </div>
); );
} }
@ -213,9 +218,13 @@ export default function AssignmentsPage() {
</div> </div>
{groupAssignments.map((assignment) => ( {groupAssignments.map((assignment) => (
<AssignmentCard <AssignmentCard
// : key "id-" , // : key id .
// ( ) . // key id ("id-none" "id-123"). key
key={`${assignment.id}-${findSubmission(assignment.id)?.id ?? 'none'}`} // React " " (),
// ' !'
// . (· Promise.all
// id key .)
key={assignment.id}
assignment={assignment} assignment={assignment}
submission={findSubmission(assignment.id)} submission={findSubmission(assignment.id)}
onSubmitted={loadMySubmissions} onSubmitted={loadMySubmissions}

View File

@ -72,14 +72,21 @@ export default function SignupPage() {
} }
// : ( ), ( ). . // : ( ), ( ). .
// " "
// 400 . · SignupRequest .
function validate() { function validate() {
if (!form.username.trim()) return '아이디를 입력해 주세요.'; if (!/^[a-zA-Z0-9]{4,}$/.test(form.username.trim()))
return '아이디는 영문·숫자만으로 4자 이상이어야 해요.';
if (form.password.length < 8) return '비밀번호는 8자 이상이어야 해요.'; if (form.password.length < 8) return '비밀번호는 8자 이상이어야 해요.';
if (form.password !== form.passwordConfirm) return '비밀번호 확인이 일치하지 않아요.'; if (form.password !== form.passwordConfirm) return '비밀번호 확인이 일치하지 않아요.';
if (!form.name.trim()) return '이름을 입력해 주세요.'; if (!form.name.trim()) return '이름을 입력해 주세요.';
if (!/^[a-z0-9._-]{2,40}$/.test(form.companyEmailLocal)) if (!/^[a-z0-9._-]{2,40}$/.test(form.companyEmailLocal))
return '회사 이메일 아이디는 소문자·숫자·. _ - 만 쓸 수 있어요.'; return '회사 이메일 아이디는 소문자·숫자·. _ - 만 쓸 수 있어요.';
if (!form.birthDate) return '생년월일을 입력해 주세요.'; if (!form.birthDate) return '생년월일을 입력해 주세요.';
// @Past · .
// ( "YYYY-MM-DD" .)
if (form.birthDate >= new Date().toISOString().slice(0, 10))
return '생년월일을 다시 확인해 주세요 — 오늘 이전 날짜여야 해요.';
if (!/^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$/.test(form.phone)) return '연락처 형식을 확인해 주세요.'; if (!/^01[0-9]-?[0-9]{3,4}-?[0-9]{4}$/.test(form.phone)) return '연락처 형식을 확인해 주세요.';
if (!form.address.trim()) return '주소를 입력해 주세요.'; if (!form.address.trim()) return '주소를 입력해 주세요.';
if (!form.school.trim()) return '학교를 입력해 주세요.'; if (!form.school.trim()) return '학교를 입력해 주세요.';

View File

@ -276,8 +276,7 @@ export default function CloudNetworkPage() {
배치합니다. 핵심 구분은 하나 <strong>인터넷에서 직접 들어올 있느냐</strong>예요. 배치합니다. 핵심 구분은 하나 <strong>인터넷에서 직접 들어올 있느냐</strong>예요.
</p> </p>
<Code>{CODE_SUBNET}</Code> <Code>{CODE_SUBNET}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li> <li>
<strong>퍼블릭 서브넷</strong> 인터넷 게이트웨이(정문) 연결된 대로변. <strong>퍼블릭 서브넷</strong> 인터넷 게이트웨이(정문) 연결된 대로변.
서버처럼 <strong>손님(사용자) 직접 맞아야 하는 </strong> 여기에 둬요. 서버처럼 <strong>손님(사용자) 직접 맞아야 하는 </strong> 여기에 둬요.
@ -288,7 +287,6 @@ export default function CloudNetworkPage() {
아무리 두드려도 애초에 길이 없어요. 아무리 두드려도 애초에 길이 없어요.
</li> </li>
</ol> </ol>
</div>
<svg className="diagram" viewBox="0 0 640 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="퍼블릭 서브넷과 프라이빗 서브넷 구조: 인터넷에서 게이트웨이(정문)를 지나 퍼블릭 서브넷의 웹 서버까지만 길이 있고, 프라이빗 서브넷의 데이터베이스로 가는 길은 없다"> <svg className="diagram" viewBox="0 0 640 300" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="퍼블릭 서브넷과 프라이빗 서브넷 구조: 인터넷에서 게이트웨이(정문)를 지나 퍼블릭 서브넷의 웹 서버까지만 길이 있고, 프라이빗 서브넷의 데이터베이스로 가는 길은 없다">
<rect x="196" y="24" width="420" height="260" rx="10" fill="none" stroke="var(--line)" strokeWidth="1.5" /> <rect x="196" y="24" width="420" height="260" rx="10" fill="none" stroke="var(--line)" strokeWidth="1.5" />
<text x="210" y="46" fontSize="12.5" fontWeight="600" fill="var(--ink)">VPC (10.0.0.0/16)</text> <text x="210" y="46" fontSize="12.5" fontWeight="600" fill="var(--ink)">VPC (10.0.0.0/16)</text>

View File

@ -313,14 +313,12 @@ export default function DebuggingPage() {
로그 하나하나가 <strong>"이 지점에서 이 값은 뭐지?"라는 질문</strong> 되도록 찍는 정석이에요. 로그 하나하나가 <strong>"이 지점에서 이 값은 뭐지?"라는 질문</strong> 되도록 찍는 정석이에요.
</p> </p>
<Code>{CODE_CONSOLE_LOG}</Code> <Code>{CODE_CONSOLE_LOG}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li><strong>라벨을 붙인다</strong> <span className="icode">'[함수명] 변수명 ='</span> 형식이면 어디서 찍힌 건지 바로 알아요.</li> <li><strong>라벨을 붙인다</strong> <span className="icode">'[함수명] 변수명 ='</span> 형식이면 어디서 찍힌 건지 바로 알아요.</li>
<li><strong>관문마다 찍는다</strong> 함수 입구·분기 직전·반환 직전. 데이터가 어디서부터 이상해졌는지 드러납니다.</li> <li><strong>관문마다 찍는다</strong> 함수 입구·분기 직전·반환 직전. 데이터가 어디서부터 이상해졌는지 드러납니다.</li>
<li><strong>객체는 통째로 찍는다</strong> 필드 이름을 기억이 아니라 실제 데이터로 확인하세요.</li> <li><strong>객체는 통째로 찍는다</strong> 필드 이름을 기억이 아니라 실제 데이터로 확인하세요.</li>
<li><strong> 잡으면 지운다</strong> 디버그 로그를 커밋에 남기면 다음 사람의 콘솔이 쓰레기장이 돼요.</li> <li><strong> 잡으면 지운다</strong> 디버그 로그를 커밋에 남기면 다음 사람의 콘솔이 쓰레기장이 돼요.</li>
</ol> </ol>
</div>
<div className="warn"> <div className="warn">
<b>주의</b> 로그가 <strong>아예 찍히는 </strong> 중요한 단서입니다! <b>주의</b> 로그가 <strong>아예 찍히는 </strong> 중요한 단서입니다!
"값이 이상하다" 아니라 "이 함수 자체가 호출되지 않는다" 뜻이니까요. "값이 이상하다" 아니라 "이 함수 자체가 호출되지 않는다" 뜻이니까요.
@ -456,13 +454,11 @@ export default function DebuggingPage() {
부끄러운 일이 아니라 <strong>팀이 알아야 정보</strong>입니다. 부끄러운 일이 아니라 <strong>팀이 알아야 정보</strong>입니다.
</p> </p>
<Code>{CODE_GOOD_QUESTION}</Code> <Code>{CODE_GOOD_QUESTION}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li><strong>하려던 </strong> 목표를 알아야 답도 목표에 맞출 있어요.</li> <li><strong>하려던 </strong> 목표를 알아야 답도 목표에 맞출 있어요.</li>
<li><strong>에러 전문</strong> 요약·스크린샷 말고 텍스트 통째로. 여러분이 지운 줄에 답이 있는 경우가 정말 많아요.</li> <li><strong>에러 전문</strong> 요약·스크린샷 말고 텍스트 통째로. 여러분이 지운 줄에 답이 있는 경우가 정말 많아요.</li>
<li><strong>재현 방법 + 시도한 </strong> 이걸 쓰다가 스스로 답을 찾는 일이 놀랄 만큼 자주 일어납니다.</li> <li><strong>재현 방법 + 시도한 </strong> 이걸 쓰다가 스스로 답을 찾는 일이 놀랄 만큼 자주 일어납니다.</li>
</ol> </ol>
</div>
<div className="tip"> <div className="tip">
<b>직접 확인해 보기 </b> 이번 주에 만난 에러 하나를 골라, 템플릿대로 <b>직접 확인해 보기 </b> 이번 주에 만난 에러 하나를 골라, 템플릿대로
<strong> Gitea(edu.awesomedevapp.com:3000) 이슈</strong> 실제로 작성해 보세요. <strong> Gitea(edu.awesomedevapp.com:3000) 이슈</strong> 실제로 작성해 보세요.

View File

@ -375,14 +375,12 @@ export default function DesignSystemPage() {
완전히 다른 경험이에요 후자를 사람만 디자인 시스템을 "만들" 있습니다. 완전히 다른 경험이에요 후자를 사람만 디자인 시스템을 "만들" 있습니다.
</p> </p>
<Code>{CODE_DZ03_TABLE}</Code> <Code>{CODE_DZ03_TABLE}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li>Gitea(edu.awesomedevapp.com:3000)에서 <strong>티켓 DZ-03</strong> 열고 본인을 담당자로 지정해요.</li> <li>Gitea(edu.awesomedevapp.com:3000)에서 <strong>티켓 DZ-03</strong> 열고 본인을 담당자로 지정해요.</li>
<li> 표의 ①번부터 순서대로 채워요 ①은 <span className="icode">global.css</span>, ·③은 <span className="kbd">F12</span> 무기예요.</li> <li> 표의 ①번부터 순서대로 채워요 ①은 <span className="icode">global.css</span>, ·③은 <span className="kbd">F12</span> 무기예요.</li>
<li>완성한 표를 DZ-03 댓글로 올리고, "우리 플랫폼에 빠져 있다고 느낀 상태·토큰 1가지" 함께 적어요.</li> <li>완성한 표를 DZ-03 댓글로 올리고, "우리 플랫폼에 빠져 있다고 느낀 상태·토큰 1가지" 함께 적어요.</li>
<li>멘토 리뷰를 받으면 실습 완료! 지적받은 부분은 표를 고쳐서 다시 올려요.</li> <li>멘토 리뷰를 받으면 실습 완료! 지적받은 부분은 표를 고쳐서 다시 올려요.</li>
</ol> </ol>
</div>
<div className="tip"> <div className="tip">
<b>막히면</b> 섹션 2 "직접 확인해 보기 ①" 돌아가서 :root 보는 법을 복습하세요. <b>막히면</b> 섹션 2 "직접 확인해 보기 ①" 돌아가서 :root 보는 법을 복습하세요.
값이 <span className="icode">var(--...)</span> 나오면 변수를 :root에서 값이 <span className="icode">var(--...)</span> 나오면 변수를 :root에서

View File

@ -218,13 +218,11 @@ export default function TcpUdpPage() {
<strong> 3-way 핸드셰이크</strong>( 번의 악수)라고 부릅니다. <strong> 3-way 핸드셰이크</strong>( 번의 악수)라고 부릅니다.
</p> </p>
<Code>{CODE_HANDSHAKE}</Code> <Code>{CODE_HANDSHAKE}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li><strong>SYN</strong> PC가 "대화 시작하고 싶어요" 하고 손을 내밀어요.</li> <li><strong>SYN</strong> PC가 "대화 시작하고 싶어요" 하고 손을 내밀어요.</li>
<li><strong>SYN+ACK</strong> 서버가 "네 잘 받았어요(ACK), 저도 시작할게요(SYN)" 하고 맞잡아요.</li> <li><strong>SYN+ACK</strong> 서버가 "네 잘 받았어요(ACK), 저도 시작할게요(SYN)" 하고 맞잡아요.</li>
<li><strong>ACK</strong> PC가 "확인했어요!" 하고 악수를 완성해요. 이제 데이터 출발!</li> <li><strong>ACK</strong> PC가 "확인했어요!" 하고 악수를 완성해요. 이제 데이터 출발!</li>
</ol> </ol>
</div>
<svg className="diagram" viewBox="0 0 640 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="TCP 3-way 핸드셰이크 시퀀스: 내 PC가 SYN을 보내고, 서버가 SYN+ACK로 답하고, 내 PC가 ACK를 보내면 연결이 수립된다"> <svg className="diagram" viewBox="0 0 640 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="TCP 3-way 핸드셰이크 시퀀스: 내 PC가 SYN을 보내고, 서버가 SYN+ACK로 답하고, 내 PC가 ACK를 보내면 연결이 수립된다">
<rect x="75" y="16" width="130" height="38" rx="8" fill="var(--card)" stroke="var(--line)" /> <rect x="75" y="16" width="130" height="38" rx="8" fill="var(--card)" stroke="var(--line)" />
<text x="140" y="40" textAnchor="middle" fontSize="14" fontWeight="600" fill="var(--ink)"> PC</text> <text x="140" y="40" textAnchor="middle" fontSize="14" fontWeight="600" fill="var(--ink)"> PC</text>
@ -390,14 +388,12 @@ export default function TcpUdpPage() {
<span className="icode">cmd</span> 엔터로 터미널을 있어요. <span className="icode">cmd</span> 엔터로 터미널을 있어요.
</div> </div>
<Code>{CODE_NETSTAT}</Code> <Code>{CODE_NETSTAT}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li>원격 주소가 <span className="icode">:443</span> 줄을 찾아보세요 전부 HTTPS 연결이에요.</li> <li>원격 주소가 <span className="icode">:443</span> 줄을 찾아보세요 전부 HTTPS 연결이에요.</li>
<li>로컬 주소의 5 번대 포트에 주목 <strong>임시 포트</strong> 자동 배정된 거예요.</li> <li>로컬 주소의 5 번대 포트에 주목 <strong>임시 포트</strong> 자동 배정된 거예요.</li>
<li><span className="icode">tasklist</span> PID의 정체를 밝혀 보세요. 대부분 브라우저일 거예요.</li> <li><span className="icode">tasklist</span> PID의 정체를 밝혀 보세요. 대부분 브라우저일 거예요.</li>
<li>브라우저를 전부 끄고 다시 실행 <span className="icode">ESTABLISHED</span> 줄이 줄어드는 관찰!</li> <li>브라우저를 전부 끄고 다시 실행 <span className="icode">ESTABLISHED</span> 줄이 줄어드는 관찰!</li>
</ol> </ol>
</div>
<p> <p>
여기서 <span className="icode">ESTABLISHED</span> 줄이 섹션 2에서 배운 여기서 <span className="icode">ESTABLISHED</span> 줄이 섹션 2에서 배운
3-way 악수가 <strong>성공적으로 끝난 흔적</strong>입니다. 참고로 UDP는 연결이라는 3-way 악수가 <strong>성공적으로 끝난 흔적</strong>입니다. 참고로 UDP는 연결이라는

View File

@ -269,14 +269,12 @@ export default function ToolIntellijPage() {
디버거는 다릅니다 <strong>시간을 멈추고 현장을 직접 걸어다니는</strong> 방식이거든요. 디버거는 다릅니다 <strong>시간을 멈추고 현장을 직접 걸어다니는</strong> 방식이거든요.
</p> </p>
<Code>{CODE_DEBUGGER}</Code> <Code>{CODE_DEBUGGER}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li>의심 가는 줄에 <strong>브레이크포인트</strong>(빨간 ) 찍고,</li> <li>의심 가는 줄에 <strong>브레이크포인트</strong>(빨간 ) 찍고,</li>
<li><strong>벌레 아이콘(Debug)</strong>으로 실행한 ,</li> <li><strong>벌레 아이콘(Debug)</strong>으로 실행한 ,</li>
<li>멈춘 순간의 <strong>Variables </strong>에서 변수의 진짜 값을 확인 <li>멈춘 순간의 <strong>Variables </strong>에서 변수의 진짜 값을 확인
상상과 다른 값이 들어 있는 지점이 바로 버그의 현장입니다.</li> 상상과 다른 값이 들어 있는 지점이 바로 버그의 현장입니다.</li>
</ol> </ol>
</div>
<p> <p>
<span className="kbd">F8</span>( 줄씩) <span className="kbd">F7</span>(메서드 안으로) <span className="kbd">F8</span>( 줄씩) <span className="kbd">F7</span>(메서드 안으로)
차이만 익혀도 충분해요. 특정 변수를 계속 지켜보고 싶으면 우클릭 차이만 익혀도 충분해요. 특정 변수를 계속 지켜보고 싶으면 우클릭

View File

@ -219,13 +219,11 @@ export default function ToolShortcutsPage() {
머릿속 생각도 잠깐 끊깁니다. 단축키가 손에 붙으면 "창 바꿔야지"라는 생각조차 없이 머릿속 생각도 잠깐 끊깁니다. 단축키가 손에 붙으면 "창 바꿔야지"라는 생각조차 없이
손가락이 먼저 움직여요 자전거 "페달을 밟아야지" 하고 생각하지 않는 것처럼요. 손가락이 먼저 움직여요 자전거 "페달을 밟아야지" 하고 생각하지 않는 것처럼요.
</p> </p>
<div className="olist"> <ol className="olist">
<ol>
<li>이번 : 섹션 2~4 표에서 <strong>내가 자주 하는 동작 5</strong> 고른다.</li> <li>이번 : 섹션 2~4 표에서 <strong>내가 자주 하는 동작 5</strong> 고른다.</li>
<li> 5개를 때마다 마우스 대신 단축키로 어색해도 참는다.</li> <li> 5개를 때마다 마우스 대신 단축키로 어색해도 참는다.</li>
<li>손에 붙으면(대략 3) 다음 5개를 추가한다.</li> <li>손에 붙으면(대략 3) 다음 5개를 추가한다.</li>
</ol> </ol>
</div>
<div className="tip"> <div className="tip">
<b>한꺼번에 30 외우려 하지 마세요</b> 단축키는 암기 과목이 아니라 <b>한꺼번에 30 외우려 하지 마세요</b> 단축키는 암기 과목이 아니라
<strong> 운동</strong>이에요. 하루 5개씩, 실제 상황에서 손으로 반복하는 <strong> 운동</strong>이에요. 하루 5개씩, 실제 상황에서 손으로 반복하는
@ -409,13 +407,11 @@ export default function ToolShortcutsPage() {
</p> </p>
<Code>{CODE_MISSION}</Code> <Code>{CODE_MISSION}</Code>
<Code>{CODE_BINGO}</Code> <Code>{CODE_BINGO}</Code>
<div className="olist"> <ol className="olist">
<ol>
<li>미션 1(마우스 없이 전환) <strong>오늘</strong> 시작한다 벌점을 메모장에 정직하게 기록.</li> <li>미션 1(마우스 없이 전환) <strong>오늘</strong> 시작한다 벌점을 메모장에 정직하게 기록.</li>
<li>빙고판을 종이에 그리거나 인쇄해서 모니터 옆에 붙인다.</li> <li>빙고판을 종이에 그리거나 인쇄해서 모니터 옆에 붙인다.</li>
<li>3 안에 빙고 3줄을 완성하면 멘토에게 자랑한다. (진짜로 자랑해도 됩니다)</li> <li>3 안에 빙고 3줄을 완성하면 멘토에게 자랑한다. (진짜로 자랑해도 됩니다)</li>
</ol> </ol>
</div>
<div className="tip"> <div className="tip">
<b>공부 </b> 단축키가 손에 붙는 데는 보통 <strong>3일의 어색함</strong> <b>공부 </b> 단축키가 손에 붙는 데는 보통 <strong>3일의 어색함</strong>
필요해요. "마우스가 더 빠른데?" 싶은 3일을 버티는 코스의 전부입니다. 필요해요. "마우스가 더 빠른데?" 싶은 3일을 버티는 코스의 전부입니다.

View File

@ -276,13 +276,11 @@ export default function ToolTerminalPage() {
모아 주는 마이크로소프트의 공식 터미널 앱이에요. 크롬에서 여러 쓰듯이, 모아 주는 마이크로소프트의 공식 터미널 앱이에요. 크롬에서 여러 쓰듯이,
탭마다 다른 셸을 띄울 있죠. 탭마다 다른 셸을 띄울 있죠.
</p> </p>
<div className="olist"> <ol className="olist">
<ol>
<li>윈도우 11이라면 이미 기본 설치돼 있어요 시작 메뉴에서 <strong>"터미널"</strong> 검색.</li> <li>윈도우 11이라면 이미 기본 설치돼 있어요 시작 메뉴에서 <strong>"터미널"</strong> 검색.</li>
<li>없다면 <strong>Microsoft Store</strong>에서 "Windows Terminal" 검색 설치. !</li> <li>없다면 <strong>Microsoft Store</strong>에서 "Windows Terminal" 검색 설치. !</li>
<li>열면 기본으로 PowerShell 탭이 하나 떠요. 줄의 <strong></strong> 버튼을 누르면 다른 목록이 나옵니다.</li> <li>열면 기본으로 PowerShell 탭이 하나 떠요. 줄의 <strong></strong> 버튼을 누르면 다른 목록이 나옵니다.</li>
</ol> </ol>
</div>
<Code>{CODE_WT_KEYS}</Code> <Code>{CODE_WT_KEYS}</Code>
<div className="tip"> <div className="tip">
<b>직접 확인해 보기</b> Windows Terminal을 열고 <span className="kbd">Alt</span> + <b>직접 확인해 보기</b> Windows Terminal을 열고 <span className="kbd">Alt</span> +