subindev 개발 블로그

[C#] 3. ToDo & 회원 가입 - ASP.NET & EF CORE (Oracle연동) 본문

.NET

[C#] 3. ToDo & 회원 가입 - ASP.NET & EF CORE (Oracle연동)

subindev 2025. 12. 2. 16:28

📝 [개발일지] ASP.NET Core Todo 프로젝트  2025/12/02(화)

 

ASP.NET Core + Oracle + EF Core 조합으로
Todo List 기능을 만들며 여러 오류를 해결하고,
C#/.NET 문법 특징도 함께 정리한 하루였다.

 

오늘 .Net 개발을 하며 겪은 다양한 오류 상황을 기록하려고 한다

다음에도 참고하기 위해 정리해둔다.


📌 1. 오늘 작업 요약

✔ .NET + EF CORE (ORACLE 연동) 투두리스트 만들기

✔ 로그인, ToDoList 기능 만들기

✔ Oracle CHAR 타입 때문에 WHERE 절이 동작하지 않는 문제 해결

✔ Oracle PK 중복(ORA-00001) 오류 해결

✔ 로그인 후 사용자 Claim 값 가져오기 문제 해결

✔ Todo 추가/토글/삭제 로직을 AJAX 기반으로 재구성

✔ C#/.NET 문법 특징 정리 (Java와 비교하며 이해)

✔ Razor(View) + Controller 구조 흐름 이해

 


📌 2. Oracle CHAR 공백 문제 — WHERE 절이 왜 안될까?

❗ 문제

DB의 WRITER_ID가 CHAR(50) 이라서
값이 '104286 ' 처럼 우측 공백을 포함함.

그래서 다음 LINQ가 제대로 동작하지 않았다:

 
.Where(t => t.Writer == userId)

둘 다 문자열 같아 보여도
DB CHAR는 오른쪽에 공백이 붙어 있다.


✔ 해결 방법: Trim() 적용

 
.Where(t => t.Writer.Trim() == userId.Trim())

Oracle 로그에서도 이렇게 변환된 걸 확인했다:

 
WHERE LTRIM(RTRIM("WRITER_ID")) = :Trim_0

📌 Oracle에서 CHAR 쓰면 무조건 Trim 필요!

→ 나중에 테이블 수정 시 VARCHAR2로 바꾸는 것이 권장된다.


📌 3. ORA-00001 오류 — PK 중복 문제 해결

Todo 추가 시 다음 오류 발생:

 
ORA-00001: unique constraint violated

❗ 원인

Oracle은 MySQL처럼 AUTO_INCREMENT가 없는데
ID 컬럼이 자동 증가가 아니었음.

EF Core가 삽입할 때 ID=0 을 계속 넣어서 PK 충돌.


✔ 해결

테이블의 ID 컬럼을 Identity로 지정:

 
ALTER TABLE TODOITEMS MODIFY ID GENERATED ALWAYS AS IDENTITY;

이후 INSERT 시 Oracle이 자동으로 PK 값을 생성하여 해결됨.


📌 4. 로그인한 사용자 정보 가져오기 문제 해결

처음에는 Claim Key를 잘못 써서
아이디/이름이 null 또는 빈 값이 나왔다.

예: User.FindFirst("UserName") 를 읽으려 했지만
실제 저장한 Claim Key는 "QM05NAME" 이었다.


✔ 해결

 
string name = User.FindFirst("QM05NAME")?.Value; string id = User.FindFirst("QMO5IPID")?.Value;

📌 Claim Key는 로그인 시 설정한 이름 그대로 읽어야 한다.

 


📌 5. Todo 로직을 AJAX 기반으로 개선

기존 구조는 다음과 같았다:

  • 추가(Add) → POST → Index Redirect
  • 토글(Toggle) → GET → DB 수정 → Redirect
  • 삭제(Delete) → GET → DB 삭제 → Redirect

➡ 모든 요청이 페이지 이동 기반이라 좋지 않았다.


✔ 해결: 모두 AJAX 기반으로 변경

(1) Todo 추가 AJAX

 
$.ajax({ url: "/Todo/Add", method: "POST", data: { Content: content }, success: () => location.reload() });

(2) 완료 체크(Toggle) AJAX

 
$(".toggle-checkbox").change(function () { $.post("/Todo/Toggle", { id, isDone }, () => location.reload()); });

(3) 삭제 AJAX

 
$(".delete-item").click(function () { $.post("/Todo/Delete", { id }, () => location.reload()); });

➡ 화면 이동 없이 자연스럽게 동작
➡ 컨트롤러 로직은 완전히 분리
(Index는 조회만 담당)

 


📌 6. C# / .NET 문법 특징 정리

오늘 작업하다 자연스레 다시 느낀 C#/.NET 문법 특징을 정리해본다.


✔ 1) C#은 PascalCase를 사용함

  • 클래스명 → PascalCase
  • 메서드명 → PascalCase
  • Property → PascalCase

예:

 
public string UserName { get; set; }

Java의 camelCase와 다름.


✔ 2) Entity 속성은 기본적으로 null 허용 X

C#의 string은 참조 타입이라 null 가능하지만,
EF Core는 Required 붙이면 자동 검증됨.

 
[Required] public string Content { get; set; }

✔ 3) new로 객체 생성 시 초기화 가능

 
var item = new TodoItem { Writer = userId, Content = model.Content };

✔ 4) Controller에서 return 종류

  • return View(model) : 화면 렌더링
  • return RedirectToAction("Index") : 이동
  • return Ok() : 200 OK
  • return BadRequest() : 400
  • return NotFound() : 404

✔ 5) .NET Authentication 흐름

  1. 로그인 성공 → ClaimsIdentity 생성
  2. CookieAuth로 사용자 정보 저장
  3. User.FindFirst() 로 어디서든 읽기 가능

✔ 6) Razor 문법

  • @variable
  • @{ ... }
  • @if(...){}
  • @model List<T> ← 중요

HTML과 C#이 섞이는 문법.


✔ 7) Authentication - 쿠키 저장 방식 (SSR 방식)

// Program.cs

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/Account/Login";   // 로그인 페이지
        options.LogoutPath = "/Account/Logout"; // 로그아웃
        options.ExpireTimeSpan = TimeSpan.FromHours(4); // 쿠키 유지시간
    });

builder.Services.AddAuthorization();

app.UseAuthentication(); // 반드시 UseAuthorization(); 앞에 존재해야함
app.UseAuthorization();
// AccountController.cs 

       // -----------------------------------------------------
       // 로그인 처리 (POST)
       // -----------------------------------------------------

       [HttpPost]
       public async Task<IActionResult> Login(LoginViewModel model)
       {
           // 유효성 검사 실패 시
           if (!ModelState.IsValid)
           {
               return View(model);
           }

           _logger.LogInformation("로그인 시도 - ID: {Id}", model.QM05IPID);
          


           // 사용자 조회
           var user = _context.Users
                                       .FirstOrDefault(u =>
                                           u.QM05IPID.Trim() == model.QM05IPID.Trim() &&
                                           u.QM05PSWD.Trim() == model.QM05PSWD.Trim());

                                           // Oracle char 과 C# string을 비교할 대 Trim 이 없다면 에러 발생
                                           // Oracle char(8) 이라면 '125641  ' 으로 저장되어 있기 때문

           if (user == null)
           {
               // 로그인 실패 메시지 전달
               ViewBag.Error = "아이디 또는 비밀번호가 올바르지 않습니다.";
               return View(model);
           }

           // 로그인 성공 → 인증 정보 쿠키에 저장
           var claims = new List<Claim>
           {
               new Claim("QM05IPID", user.QM05IPID),
               new Claim("QM05NAME", user.QM05NAME)
           };

           _logger.LogInformation("User 정보: {user}", user);

           var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
           var principal = new ClaimsPrincipal(identity);

           await HttpContext.SignInAsync(
               CookieAuthenticationDefaults.AuthenticationScheme,
               principal);

           return RedirectToAction("Todo", "Todo");
       }
// AccountController.cs

    // -----------------------------------------------------
    // TODO : 로그아웃 처리 (POST)
    // -----------------------------------------------------
    [HttpPost]
    public async Task<IActionResult> Logout()
    {
        await HttpContext.SignOutAsync(); // 쿠키에서 로그인 정보 삭제
        return RedirectToAction("Login", "Account");
    
    }

인증된 사용자 정보 사용 방법

// TodoController.cs

  public IActionResult Todo()
  {

      try
      {
          // 로그인
          var userId = User.FindFirst("QM05IPID")?.Value;
          var userName = User.FindFirst("QM05NAME")?.Value;



          
          List<TodoViewModel> list = _context.TodoItems
                              .Where(t => t.Writer.Trim() == userId.Trim())
                              .OrderByDescending(t => t.Id)
                              .Select(t => new TodoViewModel
                              {
                                  Id = t.Id,
                                  Writer = t.Writer,
                                  Content = t.Content,
                                  IsDone = t.IsDone,
                                  CreatedAt = t.CreatedAt
                              })
                              .ToList();

          return View(list);
      }

      catch (Exception e)
      {
          _logger.LogError(e, "TODO Controller - TODO 조회 중 오류 발생");
          return StatusCode(500, new { message = "서버 오류가 발생했습니다." });

      }
  }

 

📌 7. 마무리

결과 화면

 

오늘은 처음엔 단순한 Todo 기능처럼 보였지만,
중간중간 발생한 문제들이 실제로 굉장히 중요한 개념들이었다.

  • Oracle DB 타입 차이
  • PK 자동 증가 문제
  • Razor + MVC + AJAX 조합
  • java와 C# 문법 차이
  • Controller 책임 분리

이런 것들을 한 번에 경험하면서
.NET 기반 웹 개발의 전반적인 흐름을 더 깊게 이해하게 된 하루였다.

다음에 이 글을 다시 보면
오늘 어떤 문제 해결 과정을 거쳤고
왜 이런 방식으로 구조가 되었는지 빠르게 떠올릴 수 있을 것이다.

'.NET' 카테고리의 다른 글

[.NET] 2. ASP.NET Core MVC + MySQL 게시판 프로젝트 (VS Code)  (0) 2025.10.15