Competições Senac RS · Seletiva 26

Apostila teórica

Apostila Teórica — Etapa Seletiva 2026

OcupaçãoDesenvolvimento de SistemasTreinadorDiogo Roehrs (Senac RS)
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs

Competidoras: 2 (1º e 2º lugar na Regional RS 2026)


Pra que serve essa apostila

Essa é a base teórica do treino inteiro. Enquanto o cronograma diz quando a gente vê cada coisa e os Treinos Avaliativos medem o quanto evoluímos, a apostila é o o quê e o porquê: o conteúdo pra estudar, os conceitos pra fixar e as boas práticas pra transformar em automático.

A ideia não é decorar. É ter uma base sólida o suficiente pra chegar num tema inédito, no dia da prova, e resolver. Por isso cada módulo foi montado com três olhares ao mesmo tempo:

  1. O conceito de verdade, explicado do jeito que se entende.
  2. A prática de mercado: como um dev faz isso no dia a dia, com as boas práticas que o mercado espera.
  3. A realidade da competição: como aquilo cai na prova, como o CIS pontua e quais atalhos honestos (as tais "gambiarras" boas) fazem ganhar tempo sem perder qualidade.

Tudo aqui está ancorado em duas fontes oficiais: o Plano de Curso do Técnico em Desenvolvimento de Sistemas do Senac e o Descritivo Técnico da ocupação nas Competições. Não é invenção. É o que a formação já pede, trazido pra nossa realidade de competição.


Como estudar com ela


As marcações que aparecem o tempo todo

Do mesmo jeito que no cronograma, cada bloco vem marcado pela natureza do trabalho. Serve pra qualquer pessoa, técnica ou não, entender o que está sendo construído.


O mapa do conteúdo

A apostila segue a mesma ordem que o treino monta o sistema: primeiro entender como se é avaliado, depois a fundação (ambiente e banco), depois o miolo (API e front), depois as funcionalidades novas, e por fim o acabamento e a apresentação.

# Módulo UC do plano de curso Peso na prova Natureza
01 Como a prova funciona Descritivo Técnico Meta (vale pra tudo) 🎯 Foco
02 Ambiente, ferramentas e versionamento UC10 5% 🔄 Revisão
03 Banco de dados UC3 10% 🔄 Revisão
04 Back-end e API RESTful UC13 · UC9 · UC4 20% 🔄 Revisão
05 Front-end web UC12 · UC15 40% 🔄 Revisão
06 UX e interface UC16 10% 🔄🆕
07 Funcionalidades novas (Wedding Pass) UC1 · aplicação — (aplica tudo) 🆕 Novo
08 Lapidação e qualidade de código UC4 · UC11 — (acabamento) 🆕 Refino
09 Inglês técnico UC5 · UC7 15% 🔄 Revisão
10 Apresentação: pitch técnico e live demo Módulo D Parte da nota 🆕 Novo
💡Nota

Por que o front-end é o módulo mais gordo? Porque na competição ele vale 40% da nota, mais que qualquer outra competência. Banco e UX valem 10% cada, back-end 20%, inglês 15%, versionamento 5%. A apostila investe em cada assunto mais ou menos na proporção que ele pesa na prova. Estudar o que vale mais primeiro é estratégia, não preguiça.


Como isso casa com o cronograma

Fase do treino Quando Módulos pra estudar
Setup + diagnóstico Semana 1 de agosto 01, 02
A base (banco) Semana 2 de agosto 03
A base (API) Semana 3 de agosto 04
🎯 TA1 Fim de agosto 01, 03, 04
Front-end base Semana 1 de setembro 05, 06
Funcionalidades novas Semanas 2 e 3 de setembro 07
🎯 TA2 Fim de setembro Tudo até aqui
Lapidação e modularização Outubro 08
Apresentação Fim de outubro 10
🎯 TA3 Fim de outubro Tudo
Inglês técnico Em paralelo, o treino todo 09

O inglês técnico não tem uma "semana dele". Ele aparece o tempo todo, porque documentação boa é em inglês e o mercado inteiro funciona nele. Por isso o módulo 09 é pra manter aberto ao longo de todo o treino, um pouco por dia.


Um lembrete sobre o que a gente está fazendo aqui

O nosso contexto é competição, mas o alvo é o mundo real. Toda boa prática que está aqui é a mesma que um dev usa no trabalho. Todo atalho que a gente marca como 🛠️ gambiarra boa é o tipo de coisa que dev experiente faz pra entregar mais rápido sem quebrar nada. A gente treina pro pódio, mas o que fica é repertório de profissional. É esse o combo: educacional de verdade, com o olho na prova.

Competições Senac RS · Seletiva 26

Módulo 01

Módulo 01 — Como a prova funciona

Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Por que esse é o primeiro módulo. Antes de estudar qualquer conteúdo técnico, você precisa saber como vai ser avaliada. Estudar sem saber onde a nota mora é treinar no escuro. Esse módulo é o mapa: o formato da prova, como a banca pontua e o que isso muda no jeito de trabalhar.

Base: Descritivo Técnico da ocupação (Competições Senac de Educação Profissional, 5ª edição) e o CIS (o padrão de avaliação das competições).


1. O formato do Projeto-Teste

A prova da Seletiva é um Projeto-Teste: um sistema completo, dividido em módulos, feito sob cronômetro. Na ocupação de Desenvolvimento de Sistemas ele tem 4 módulos, somando até 10 horas e 30 minutos distribuídas em 3 dias. O tema é inédito e só é conhecido na hora (com no máximo 30% de alteração revelado na reunião de alinhamento, dois dias antes).

O tema da nossa preparação é o ecossistema Wedding Pass: um sistema de gestão de casamento (convidados, convites, confirmação de presença, check-in no dia, painel de controle). Mas atenção: o tema real da prova pode ser outro. A gente treina no casamento porque foi o da Regional, mas a habilidade tem que ser transferível. Chegar num tema novo e resolver.

Os 4 módulos

Módulo Duração Dia O que é
A — Prova Surpresa 2h 1º dia Um desafio-relâmpago, revelado na hora. Testa base sólida e capacidade de resolver o inesperado.
B — Banco + API (back-end) 2h30 1º dia Modelar o banco e construir a API RESTful completa. A fundação de dados do sistema.
C — Front-end (Full Stack / SPA) 4h 2º dia A interface web que consome a API. O módulo mais longo e mais pesado.
D — Apresentação individual 2h 3º dia Pitch técnico + demonstração ao vivo do sistema, em português, até 10 minutos.
🎯Foco de prova

🎯 Leia a tabela olhando pro relógio. O front (Módulo C) tem 4 horas, quase o dobro do back. Isso não é acaso: bate com o peso de 40% do front na nota. Onde tem mais tempo e mais peso, é onde mais se ganha e mais se perde. Gestão de tempo entre os módulos é parte da prova.

O que cada módulo cobra (visão de conteúdo)

Cada módulo tem detalhes próprios no módulo correspondente dessa apostila. Aqui o ponto é enxergar o todo.


2. Como a banca pontua: o CIS

O CIS (Sistema de Informação da Competição, no padrão WorldSkills) é a planilha que transforma trabalho em nota. Ele funciona com dois tipos de avaliação, e entender a diferença muda tudo na hora de trabalhar.

2.1 Avaliação Objetiva (M) — o sim ou não

A avaliação objetiva mede se um aspecto foi ou não foi executado. É concreta, sem opinião. Ou tem, ou não tem. Precisa de pelo menos três avaliadores concordando. Ela vem em duas formas:

🎯Foco de prova

🎯 O que isso significa na prática. Objetivo é a parte "mais fácil" de garantir, porque é binária e você sabe exatamente o que precisa existir. Um check-in que bloqueia entrada dupla ou existe e funciona (ponto), ou não (zero). Por isso, os pontos objetivos são os primeiros que você trava. São a base da nota. Deixar um ponto objetivo na mesa é o erro mais burro que tem, porque não dependia de talento, dependia de lembrar de fazer.

2.2 Avaliação por Julgamento (J) — a escala 0 a 3

O julgamento aproxima a prova dos padrões do mercado. Aqui três avaliadores dão uma nota de 0 a 3 pra cada aspecto, usando uma referência combinada:

Nota O que significa
0 Não atende ao padrão exigido.
1 Atende parcialmente ao padrão.
2 Atende ao padrão profissional.
3 Atende com excelência.
🎯Foco de prova

🎯 A diferença entre 2 e 3 é o nosso jogo inteiro. As duas competidoras já entregam nível 2 em quase tudo (por isso foram 1º e 2º na Regional). "Fazer funcionar" dá 2. O 3 é o acabamento: a mensagem de erro clara, a máscara no campo, o estado de carregamento, o código organizado, o detalhe que mostra cuidado profissional. O treino inteiro é sobre transformar 2 em 3. É por isso que outubro é um mês só de lapidação.

2.3 Como os pesos entram

A nota final não trata todas as competências igual. O Descritivo Técnico define pesos, e o esquema de pontuação respeita esses pesos:

Competência Peso
Front-end web 40%
Back-end web + banco 20%
Inglês técnico 15%
UX / interface pra experiência do usuário 10%
Banco de dados 10%
Configuração e versionamento 5%

Some tudo e dá 100%. A leitura estratégica: um ponto de acabamento no front vale mais que um ponto no versionamento. Não é pra ignorar o versionamento (5% também decide medalha em prova apertada), mas é pra saber onde colocar energia quando o tempo aperta.


3. O que o formato muda no jeito de trabalhar

Saber como se é avaliada não é curiosidade, é estratégia. Três consequências práticas:

1. Trave o objetivo antes de caprichar no julgamento. No começo de cada módulo, faça primeiro tudo que é sim/não (existe/não existe): senha criptografada, JWT funcionando, CORS liberado, os CRUDs respondendo, o check-in bloqueando entrada dupla. Isso é nota garantida. Só depois volte pra lapidar o que é julgamento (organização, mensagens, acabamento).

2. Pense em "o que a banca vê". O avaliador não está dentro da sua cabeça. Ele vê o que aparece na tela e o que está no código. Uma validação que existe mas não dá feedback nenhum na tela pode não pontuar no front, mesmo funcionando. Fazer o certo e mostrar que fez.

3. Gestão de tempo é conteúdo. 10h30 em 3 módulos técnicos é pouco pro escopo. Quem não controla o relógio deixa o Módulo C (o de maior peso) pela metade. Nos ensaios de novembro a gente treina exatamente isso: quanto tempo dar pra cada parte.


4. Autoavaliação do módulo

Se você responde sem consultar, fixou:

  1. Quantos módulos tem o Projeto-Teste e quanto tempo total você tem?
  2. Qual módulo tem o maior peso e por quê ele tem mais tempo?
  3. Qual a diferença entre avaliação objetiva direta e por dedução?
  4. Na escala de julgamento, o que separa a nota 2 da nota 3?
  5. Se o tempo está acabando, você caprica no front ou no versionamento? Por quê?
  6. Por que faz sentido travar os pontos objetivos antes de lapidar os de julgamento?
💡Nota

Fechou o módulo? O próximo é o 02 — Ambiente, ferramentas e versionamento, onde a gente monta a base pra conseguir trabalhar rápido e não perder aqueles 5% bobos de versionamento.

Competições Senac RS · Seletiva 26

Módulo 02

Módulo 02 — Ambiente, ferramentas e versionamento

BaseUC10 do plano de curso (Realizar configuração e versionamento de software, 36h)Peso na prova5%NaturezaRevisão (com aprofundamento real de Git)
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Por que começar por aqui. Ambiente e versionamento não dão glória, mas dão base. Um ambiente afiado faz você trabalhar rápido nas 10h30 de prova, e o versionamento bem feito vale 5% da nota e, mais importante, é a rede de segurança: se algo quebra, você volta pra versão que funcionava em segundos. Tem ainda um terceiro papel que quase ninguém enxerga: o histórico do Git é evidência pra banca. Um histórico que conta a história do projeto mostra método, e método é o que a avaliação de julgamento procura. Esse é o primeiro assunto do treinamento (Seg 03/08) porque tudo que vem depois passa por ele: o banco vai ser versionado, a API vai ser versionada, o front vai ser versionado.


1. O ambiente das duas stacks

A gente treina com duas stacks de back-end (Node.js + Express e PHP), as duas falando com MySQL, e um front único em HTML/CSS/JS. Na prova você usa a que estiver liberada e mais afiada. O ambiente precisa estar pronto e idêntico nas duas máquinas: mesma versão de tudo, mesmas extensões, mesmos atalhos. Máquinas diferentes geram o clássico "na minha funciona", que é tempo perdido.

1.1 O que instala e como conferir

Ferramenta Pra quê Como conferir que funciona
Node.js LTS (traz o npm junto) Runtime do back em JavaScript node -v e npm -v respondem versão
PHP 8+ Runtime do back em PHP php -v responde versão
Composer Gerenciador de pacotes do PHP composer -V responde versão
MySQL Server 8 O banco das duas stacks mysql -u root -p loga no console
MySQL Workbench (ou DBeaver) Ver tabelas e dados sem decorar console Conecta no servidor local
Git Versionamento git --version responde versão
VS Code Editor Abre, formata, tem as extensões da tabela 1.2
Postman (ou Insomnia) Testar a API sem depender do front Faz um GET em qualquer URL pública
Navegador com DevTools Console, aba Network, inspecionar elemento F12 abre e você sabe onde está cada aba

Depois de instalar, configurar o Git uma vez (identidade que assina cada commit):

git config --global user.name "Seu Nome"
git config --global user.email "seu@email.com"
git config --global init.defaultBranch main
🛠️Gambiarra boa

🛠️ Gambiarra boa: o servidor embutido do PHP. Não precisa de Apache nem XAMPP configurado pra desenvolver: php -S localhost:8000 na pasta do projeto sobe um servidor local na hora. Pra prova e pra treino é mais que suficiente, e elimina uma camada inteira de configuração que pode dar problema.

🛠️ Gambiarra boa: nodemon no Node. npm install --save-dev nodemon e rodar com npx nodemon app.js: o servidor reinicia sozinho a cada arquivo salvo. Sem isso você vai esquecer de reiniciar depois de uma mudança e perder minutos caçando um bug que não existe.

1.2 VS Code: extensões e configurações que pagam o tempo investido

Extensão é ferramenta, não enfeite. As que valem pra nossa realidade:

Extensão Pra quê
Prettier Formata HTML/CSS/JS num padrão único com um atalho
PHP Intelephense Autocompletar e destaque de erro pra PHP
Live Server Sobe o front com recarga automática ao salvar
ESLint Aponta erro de JS antes de você rodar
GitLens (opcional) Mostra quem mudou cada linha e quando, direto no editor

Duas configurações que mudam o jogo (Ctrl+, abre as configurações):

1.3 Atalhos: velocidade que vira escopo entregue

Boa parte da velocidade de dev é não usar o mouse pra tudo. Os que valem treinar até virarem automáticos:

Atalho (Windows) O que faz
Ctrl+P Abre qualquer arquivo pelo nome, sem caçar na árvore
Ctrl+D Seleciona a próxima ocorrência igual (multi-cursor)
Alt+clique Cria cursores em vários pontos ao mesmo tempo
F2 Renomeia a variável/função no projeto inteiro
Ctrl+Shift+F Busca em todos os arquivos do projeto
Shift+Alt+F Formata o arquivo inteiro
Ctrl+' (crase) Abre o terminal dentro do editor
Ctrl+B Esconde/mostra a barra lateral (mais tela pro código)
🎯Foco de prova

🎯 Isso conta na prática. Cada minuto que você não gasta caçando arquivo com o mouse é um minuto pro Módulo C, que é onde a nota está. Produtividade de ambiente não aparece direto na planilha, mas aparece indireto: em quanto do escopo você conseguiu terminar.

1.4 O fluxo de montagem: subir um projeto do zero

Nos primeiros minutos da prova, quem tem o caminho de montagem decorado sai na frente. Não é decorar código, é decorar o fluxo. As duas sequências, lado a lado:

Node.js + Express:

mkdir wedding-pass && cd wedding-pass
git init
npm init -y
npm install express mysql2
npm install --save-dev nodemon
# cria app.js com o esqueleto do servidor
# cria .gitignore (seção 5)
git add .
git commit -m "estrutura inicial do projeto Node com Express e mysql2"

PHP:

mkdir wedding-pass && cd wedding-pass
git init
mkdir public src
# cria public/index.php com o roteador básico
# cria src/conexao.php com o PDO
# cria .gitignore (seção 5)
git add .
git commit -m "estrutura inicial do projeto PHP com PDO"
php -S localhost:8000 -t public

O esqueleto de código de cada uma (servidor mínimo, conexão com o banco) é assunto do módulo 04. Aqui o ponto é o ritual: pasta → git init → esqueleto → .gitignore → primeiro commit. Sempre nessa ordem, sempre com o Git nascendo junto com o projeto, nunca depois.

⚠️Pega-ratão

⚠️ Pega-ratão: deixar o git init "pra quando o projeto estiver andando". O versionamento só protege o que ele viu acontecer. Projeto que nasce sem Git tem uma janela inteira de trabalho sem rede de segurança e sem evidência.


2. Versionamento: a máquina do tempo (e a testemunha)

Versionamento é a máquina do tempo do código. Cada vez que você salva um marco (um commit), o Git guarda uma foto do projeto inteiro naquele instante. Você pode voltar pra qualquer foto, comparar o que mudou entre duas, e trabalhar em ideias novas sem medo de estragar o que já funciona.

No mercado isso é inegociável: nenhum time trabalha sem controle de versão. Na prova, são três papéis ao mesmo tempo:

  1. Rede de segurança. Quebrou tudo às 3h40 de prova? Volta pro último commit que funcionava e perde minutos, não a prova.
  2. Competência avaliada. Os 5% de configuração e versionamento saem direto da qualidade do teu repositório.
  3. Evidência de método. A banca pode ler o histórico. Vinte commits pequenos com mensagens claras contam a história de alguém que trabalha com método. Um commit gigante "projeto final" não conta história nenhuma.

3. Os três estados: como o Git enxerga teus arquivos

Aqui mora a base de tudo. Todo arquivo num repositório Git está em um de três lugares:

   Working Directory  ──git add──▶  Staging Area  ──git commit──▶  Repositório
   (teus arquivos,                  (a "área de                   (histórico
    como estão agora)                embarque":                    permanente
                                     o que vai entrar              de commits)
                                     no próximo commit)

Os comandos que movem os arquivos entre os estados:

git status                  # onde está cada arquivo agora (o comando mais usado do Git)
git add arquivo.js          # move arquivo do working pra staging
git add .                   # move tudo que mudou pra staging
git commit -m "mensagem"    # grava a staging no histórico
git diff                    # o que mudou e AINDA NÃO está na staging
git diff --staged           # o que está na staging e vai entrar no próximo commit
⚠️Pega-ratão

⚠️ Pega-ratão clássico dos três estados: editar um arquivo, dar git add, editar o mesmo arquivo de novo e commitar. O commit leva a versão que estava na staging na hora do add, não a mais recente do disco. O git status mostra o arquivo nas duas listas ao mesmo tempo ("staged" e "modified") quando isso acontece. Regra prática: git status antes de todo commit, sempre.

🛠️ Gambiarra boa: staging seletiva pra commits limpos. Mexeu em três coisas diferentes de uma vez (aconteceu, não era o ideal)? Não precisa commitar tudo junto num commit bagunçado. git add só dos arquivos de uma mudança, commita com a mensagem certa, depois add e commit do resto. O histórico sai limpo mesmo quando o trabalho não foi.


4. Commits que contam história

4.1 A anatomia de um commit

Cada commit guarda: um hash (o endereço único, tipo a3f9c21), o autor (o user.name e user.email que você configurou), a data, a mensagem, e um ponteiro pro commit anterior. Essa corrente de ponteiros é o histórico. Pra ver:

git log                      # histórico completo, um bloco por commit
git log --oneline            # uma linha por commit (o dia a dia)
git log --oneline --graph --all   # o desenho dos branches (seção 7)

4.2 O que faz uma mensagem boa

Uma boa mensagem completa a frase "este commit...": começa com verbo no presente, diz o que mudou e, quando não for óbvio, por quê. É escrita pra alguém ler daqui a duas semanas (e esse alguém é você, ou a banca).

❌ Mensagem ruim ✅ Mensagem boa
att adiciona validação de e-mail no cadastro de convidado
ajustes corrige capacidade da mesa que aceitava número negativo
wip cria tela de login com feedback de erro
agora vai corrige check-in duplicado: valida antes de inserir
mudanças no banco adiciona UNIQUE em convite pra impedir RSVP duplo

Um padrão leve de prefixo ajuda a bater o olho no git log --oneline e entender o histórico (o mercado chama de conventional commits; pra nós, a versão enxuta basta):

🎯Foco de prova

🎯 Frequência é critério de julgamento. Não existe número mágico, mas existe um ritmo: um commit por marco que funciona. Fechou o login? Commit. O CRUD respondeu? Commit. A tela renderizou certo? Commit. Numa prova de 4 horas isso dá naturalmente algo entre 8 e 15 commits, e um histórico assim é a diferença entre nota 1 e nota 3 no critério.

🛠️ Gambiarra boa: commit é checkpoint de videogame. Toda vez que uma parte passa a funcionar, commita antes de começar a próxima. Se a mudança seguinte quebrar tudo, git restore . te devolve pro último checkpoint em dois segundos (seção 6). Sob pressão de prova, esse hábito já salvou muita gente de refazer uma hora de trabalho.


5. .gitignore: o que nunca entra no repositório

Nem tudo que está na pasta do projeto deve ser versionado. Três categorias ficam de fora:

  1. O que é gerado/baixado: node_modules/ (Node) e vendor/ (PHP) são reconstruíveis com npm install / composer install. Versionar isso incha o repositório com milhares de arquivos que não são teus.
  2. O que é segredo: arquivo .env com senha do banco e chave do JWT. Nunca entra no Git.
  3. O que é lixo do sistema: .DS_Store, Thumbs.db, logs.

O .gitignore é um arquivo de texto na raiz do projeto listando o que o Git deve fingir que não existe. O nosso, que serve pras duas stacks:

node_modules/
vendor/
.env
*.log
.DS_Store
Thumbs.db
⚠️Pega-ratão

⚠️ Pega-ratão: o .gitignore não é retroativo. Se você commitou o node_modules/ e depois criou o .gitignore, o Git continua rastreando o que já entrou. Pra tirar do rastreio sem apagar do disco: git rm -r --cached node_modules/ e commita. Por isso o .gitignore nasce no primeiro commit, junto com o projeto (o fluxo da seção 1.4).

🎯 Senha commitada é ponto perdido em segurança. Se a banca abre o repositório e encontra a senha do banco num commit, isso conversa direto com os critérios de segurança (módulo 04). Configuração sensível vai no .env, o .env vai no .gitignore, e o projeto ganha um .env.example versionado só com os nomes das variáveis, sem os valores.


6. Desfazer sem drama

Errar faz parte; o Git existe pra isso. O mapa de "deu ruim → comando":

Situação Comando O que acontece
Editei um arquivo e me arrependi (não commitei) git restore arquivo.js Arquivo volta a ser como no último commit. A edição se perde.
Quebrei tudo, quero voltar pro último checkpoint git restore . Todos os arquivos voltam pro último commit
Dei git add sem querer git restore --staged arquivo.js Sai da staging, a edição continua no disco
Errei a mensagem do commit que acabei de fazer git commit --amend -m "mensagem certa" Reescreve o último commit
Quero ver como o projeto estava num commit antigo git log --oneline + git diff a3f9c21 Compara o agora com aquele ponto
Um commit antigo introduziu um bug git revert a3f9c21 Cria um commit novo desfazendo aquele. O histórico fica intacto
⚠️Pega-ratão

⚠️ Pega-ratão: git reset --hard. Esse comando existe e você vai ver na internet. Ele apaga commits e mudanças de verdade, sem lixeira. Na prova, o desfazer seguro é git restore (pra mudanças) e git revert (pra commits): os dois nunca destroem histórico. reset --hard só com o professor do lado e certeza absoluta.


7. Branch: linhas de trabalho paralelas

7.1 O conceito

Um branch é uma linha de trabalho paralela. Tecnicamente é só um ponteiro pra um commit (por isso criar branch é instantâneo e de graça), mas na prática é isso: você cria um branch pra desenvolver uma funcionalidade nova sem tocar na versão estável. A main é o tronco, a versão que sempre funciona e da qual sai a demo.

git branch                        # lista os branches (o * mostra onde você está)
git switch -c feature/check-in    # cria o branch E já muda pra ele
git switch main                   # volta pra main
git branch -d feature/check-in    # apaga o branch (depois do merge)

Nomes que se explicam: feature/rsvp-convidado, feature/dashboard, fix/capacidade-mesa. O prefixo diz o tipo, o resto diz o assunto.

7.2 O fluxo que a prova valoriza

O padrão profissional, que é o mesmo que o CIS quer ver:

  1. A main está funcionando. Nunca se desenvolve funcionalidade nova direto nela.
  2. git switch -c feature/nome-da-funcionalidade
  3. Trabalha ali, commitando em marcos pequenos.
  4. Funcionou e foi testado? git switch main e git merge feature/nome-da-funcionalidade.
  5. A main continua sempre apresentável, e é dela que sai a demo do Módulo D.
🎯Foco de prova

🎯 No nosso cronograma isso tem endereço. No primeiro dia (Seg 03/08) a gente organiza a main e a main-teste de cada uma: a main é o tronco sagrado, e a main-teste é o branch de experimento, onde vale testar ideia, quebrar e bagunçar sem medo. E na semana de 14 a 18/09, o comprovante em PDF nasce num branch com merge de propósito, pra rotina de branch → commit → merge estar no músculo bem antes da prova.


8. Merge: juntando as linhas (e resolvendo o conflito real)

8.1 Os dois tipos de merge

Fast-forward: a main não andou desde que o branch nasceu. O Git só desliza o ponteiro pra frente. Sem drama, sem commit extra:

main:     A──B
                \
feature:         C──D        →  merge  →   main: A──B──C──D

Merge commit (three-way): a main também andou enquanto o branch existia. O Git cria um commit de merge que junta as duas linhas:

main:     A──B──────E
                \     \
feature:         C──D──M   ← commit de merge

Os dois são normais e corretos. O merge commit ainda tem um bônus: aparece no git log --graph como prova visual de que você trabalhou com branches.

8.2 Conflito: o que é de verdade

Conflito acontece quando as duas linhas de trabalho mexeram na mesma região do mesmo arquivo e o Git não tem como decidir sozinho qual versão fica. Não é erro, não é castigo: é o Git pedindo uma decisão humana.

Cenário concreto no Wedding Pass: na main você ajustou o limite de convidados por mesa pra 8 no config.js; no branch feature/dashboard, o mesmo arquivo ganhou o limite 10. Na hora do merge:

$ git merge feature/dashboard
Auto-merging config.js
CONFLICT (content): Merge conflict in config.js
Automatic merge failed; fix conflicts and then commit the result.

O Git marca o trecho conflitado dentro do arquivo:

<<<<<<< HEAD
const LIMITE_MESA = 8;        // o que está na main (onde você está)
=======
const LIMITE_MESA = 10;       // o que veio do branch
>>>>>>> feature/dashboard

8.3 Resolvendo, passo a passo

  1. Respira. O projeto não quebrou; o Git está esperando você decidir.
  2. git status mostra quais arquivos estão em conflito (both modified).
  3. Abre o arquivo no VS Code. Ele destaca o trecho e oferece os botões Accept Current (fica o da main), Accept Incoming (fica o do branch) e Accept Both. Ou edita na mão: apaga os marcadores <<<<<<<, =======, >>>>>>> e deixa o código final como deve ser (às vezes a resposta certa é uma mistura dos dois).
  4. Testa. Arquivo resolvido tem que rodar. Resolver conflito escolhendo errado compila do mesmo jeito, mas quebra o comportamento.
  5. git add config.js e git commit (o Git já sugere a mensagem de merge). Pronto, as linhas se juntaram.

Se enrolar de vez: git merge --abort cancela o merge e devolve tudo pro estado de antes. Ninguém se machucou, tenta de novo com calma.

🛠️Gambiarra boa

🛠️ Gambiarra boa: conflito se treina em laboratório. Dá pra fabricar um conflito de propósito em 2 minutos: cria um repo, commita um arquivo, cria um branch, muda a linha 1 e commita, volta pra main, muda a mesma linha 1 pra outra coisa e commita, e faz o merge. Quem já resolveu dez conflitos de treino não trava quando aparece um na prova.

⚠️ Pega-ratão: começar um merge com mudanças não commitadas no working directory. Mistura o que era teu com o que veio do merge e vira um nó. Regra: working limpo antes de todo merge (git status sem nada vermelho, commit ou git stash antes).


9. Remoto, GitHub e a tal da nuvem

Tudo até aqui aconteceu no repositório local, na tua máquina. Um remoto (GitHub, GitLab) é uma cópia do repositório hospedada fora dela:

git remote add origin https://github.com/usuario/wedding-pass.git
git push -u origin main       # primeira vez: envia e vincula o branch
git push                      # daí em diante: envia os commits novos
git pull                      # traz o que mudou no remoto
git clone URL                 # baixa um repositório inteiro

Pra nossa realidade:


10. Integração contínua: o conceito que fecha a UC10

Integração contínua (CI) é a prática de juntar o trabalho de todo mundo no tronco com frequência (diária, no mínimo) e rodar uma verificação automática a cada junção: o servidor de CI pega o código, instala, builda e roda os testes. Quebrou? O time fica sabendo em minutos, não na véspera da entrega. No GitHub isso tem nome de GitHub Actions; um arquivo de configuração no repositório dispara os testes a cada push.

Numa prova individual e curta não tem servidor de CI, mas a disciplina que o CI automatiza é exatamente o nosso ritual manual:

No time com CI Na prova, versão manual
Integra pequeno e frequente Commit pequeno por marco que funciona
Build e teste automáticos a cada push Testar antes de commitar, sempre
Quebrou o build, conserta já Quebrou, volta pro checkpoint e conserta já
🎯Foco de prova

🎯 Isso rende na apresentação. "Eu trabalhei com commits pequenos e testados, que é a disciplina que a integração contínua automatiza nas equipes" é uma frase de 5 segundos no Módulo D que mostra que você entende o conceito além do decoreba. A banca ouve UC10 inteira nessa frase.


11. Como o CIS pontua versionamento

O versionamento aparece com peso 5%, misturando critério objetivo e julgamento. O mapa completo, agora com a régua 0-3 explícita:

O que a banca olha Tipo Como garantir
Existe um repositório Git com histórico? Objetivo (sim/não) git init no primeiro minuto, commitar desde o começo
Os commits são frequentes e com marcos claros? Julgamento (0-3) Um commit por funcionalidade fechada, não só no fim
As mensagens descrevem as mudanças? Julgamento (0-3) O que + por quê, prefixos, zero "att/ajustes"
Uso de branch e merge pra funcionalidades? Julgamento (0-3) Branch por funcionalidade, merge na main testado

E a régua de julgamento na prática, do zero ao três:

⚠️Pega-ratão

⚠️ Pega-ratão clássico: deixar pra "organizar o Git no fim". Aí sobra um commit gigante, com mensagem "projeto pronto", e o histórico não conta história nenhuma. Nota 0 ou 1 num critério que era dos mais fáceis de tirar 3. Commitar durante o trabalho, não depois. E não existe "arrumar o histórico depois": a data e a ordem dos commits ficam gravadas.


12. Autoavaliação do módulo

  1. O que precisa estar instalado e testado no ambiente antes de a prova começar, e qual o comando que confere cada item?
  2. Qual é o fluxo de montagem de um projeto do zero, na ordem certa, e por que o git init vem antes do código?
  3. Explica os três estados do Git e qual comando move um arquivo de cada estado pro próximo.
  4. Editei um arquivo, dei git add, editei de novo e commitei. O que foi pro commit e como o git status teria me avisado?
  5. O que faz uma boa mensagem de commit? Transforma "mexi no banco" numa mensagem nota 3.
  6. Por que node_modules/, vendor/ e .env nunca entram no repositório, e o que fazer se um deles já foi commitado?
  7. Qual a diferença entre git restore, git revert e git reset --hard, e qual deles nunca usar sozinho na prova?
  8. Qual a diferença entre um merge fast-forward e um merge commit?
  9. O que é um conflito de merge, por que ele acontece e quais os 5 passos pra resolver?
  10. O que faz o git merge --abort e quando usar?
  11. Pra que serve a main-teste no nosso fluxo e o que a main tem que ser sempre?
  12. O que é integração contínua e qual é a "versão manual" dela numa prova individual?
  13. Descreve um histórico de commits nota 3 e um nota 1 no critério de julgamento do CIS.

Referências do módulo

Bibliografia da UC10 (plano de curso):

Mercado e documentação:


💡Nota

Fechou? Ambiente afiado, Git no músculo. Agora vamos pra fundação de dados, onde o projeto inteiro se apoia: 03 — Banco de dados.

Competições Senac RS · Seletiva 26

Módulo 03

Módulo 03 — Banco de dados

BaseUC3 do plano de curso (Planejar e administrar banco de dados, 84h)Peso na prova10% direto (mas contamina os 20% de back e boa parte do front)NaturezaRevisão com aprofundamento
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. No Módulo B da prova (Banco + API, 2h30), a primeira coisa que se faz é modelar o banco. Ele é a fundação: se o modelo de dados está torto, a API vem torta, o front vem torto, e não tem acabamento que salve. O banco vale 10% direto, mas contamina os outros 60% de back e front. Aqui a natureza é 🔄 revisão, porque as duas já modelaram banco na Regional. Só que agora a revisão desce fundo: não é decorar comando, é entender por que cada decisão de modelagem existe, escolher certo em situações diferentes e conseguir defender a escolha na apresentação. Modelar certo, rápido e sabendo o porquê. Esse é o nível 3.


1. Tipos de banco de dados: relacional, não relacional e por que a prova é relacional

1.1 O que é um SGBD

Banco de dados é o dado organizado. SGBD (Sistema Gerenciador de Banco de Dados) é o software que administra esse dado: recebe comandos, garante as regras, controla acesso simultâneo, protege contra corrupção. MySQL, PostgreSQL, SQL Server e Oracle são SGBDs. Quando a gente fala "o banco barrou o insert", quem barrou foi o SGBD aplicando uma regra que você definiu.

O SQL (Structured Query Language) nasceu nos anos 70 na IBM (projeto System R, baseado no modelo relacional que o Edgar Codd propôs em 1970) e virou padrão ANSI/ISO nos anos 80. Por isso o grosso do SQL funciona igual em qualquer banco relacional: o SELECT que você escreve no MySQL é praticamente o mesmo do PostgreSQL. O que muda são detalhes de função e de tipo, e a gente vai trabalhar sempre no dialeto do MySQL 8, que é o da prova.

1.2 Relacional: tabelas, relações e garantias

No modelo relacional o dado mora em tabelas (linhas e colunas), as tabelas se ligam por chaves, e o SGBD garante um pacote de propriedades conhecido como ACID:

Propriedade O que garante Exemplo no Wedding Pass
Atomicidade Operação em grupo ou acontece inteira ou não acontece Criar convidado + convite juntos: se o convite falhar, o convidado não fica órfão
Consistência O banco nunca fica num estado que viola as regras Nenhum convite aponta pra convidado inexistente
Isolamento Duas operações simultâneas não se atropelam Dois check-ins do mesmo código ao mesmo tempo: só um passa
Durabilidade Confirmou, tá gravado, mesmo se faltar luz O check-in feito às 19h32 não some

Essas quatro letras são exatamente o que um sistema de gestão precisa. Guarda esse quadro: ele volta na seção de transações (17) e no check-in do módulo 07.

1.3 Não relacional (NoSQL): o que existe do outro lado

O plano de curso pede o comparativo, e o mercado usa os dois. NoSQL não é "melhor" nem "pior": é um conjunto de modelos diferentes pra problemas diferentes.

Família Como guarda Exemplo Brilha quando
Documento JSON aninhado, sem esquema fixo MongoDB Dados semiestruturados que mudam de forma (catálogo de produtos com atributos variados)
Chave-valor Um valor por chave, acesso direto Redis Cache, sessão, contador em tempo real
Colunar Colunas agrupadas, escrita massiva Cassandra Volume gigante de escrita (telemetria, logs)
Grafo Nós e arestas Neo4j Relacionamentos profundos (rede social, recomendação)

O trade-off central: NoSQL costuma abrir mão de parte das garantias ACID e dos JOINs pra ganhar flexibilidade e escala horizontal. Num sistema de gestão (convidados, convites, mesas, check-ins), essa troca é péssima: os dados são estruturados, se relacionam o tempo todo, e os relatórios (dashboard!) nascem de cruzamento de tabelas.

🎯Foco de prova

🎯 Por que a prova é relacional. Todo Projeto-Teste da nossa área é um sistema de gestão: entidades bem definidas, regras de integridade, relatórios agregados. Isso é o caso de uso clássico do relacional. Saber explicar isso em uma frase ("escolhi relacional porque os dados são estruturados e se relacionam, e eu preciso de integridade e de relatórios com JOIN") é repertório de apresentação que impressiona banca. NoSQL na prova é isso: repertório pra conversa, não ferramenta pra usar.


2. Os três níveis de modelagem: conceitual → lógico → físico

Modelar não é um passo só. O caminho profissional (e o que o plano de curso cobra) tem três níveis, cada um respondendo uma pergunta diferente:

Nível Pergunta Produto Se preocupa com
Conceitual O que o sistema precisa lembrar? MER (entidades, atributos, relacionamentos) O negócio. Zero tecnologia
Lógico Como isso vira tabelas? Tabelas, chaves, cardinalidades resolvidas Estrutura relacional. N:N vira tabela associativa aqui
Físico Como isso roda no MySQL? Script SQL com tipos exatos, constraints, índices O SGBD escolhido

2.1 O mesmo caminho numa situação diferente: sistema de escola

Pra provar que o método serve pra qualquer enunciado (e a Prova Surpresa vai trazer um enunciado que ninguém conhece), vamos modelar uma escola do zero:

Conceitual. Lendo o enunciado imaginário ("a escola tem turmas, alunos se matriculam em turmas, cada turma tem um professor responsável e as notas dos alunos ficam registradas"), sublinho os substantivos que o sistema precisa lembrar: aluno, turma, professor, matrícula, nota. Relações: professor 1:N turma (um professor responde por várias turmas), aluno N:N turma (um aluno em várias turmas, uma turma com vários alunos), e a nota pertence... a quê? Não ao aluno sozinho nem à turma sozinha: à matrícula (aquele aluno naquela turma). Isso é achado de modelagem: a nota é atributo do relacionamento.

Lógico. O N:N aluno-turma vira a tabela matricula (aluno_id, turma_id), e a nota vira coluna dela (ou tabela avaliacao ligada à matrícula, se forem várias notas). turma ganha professor_id. Todas as tabelas ganham chave primária.

Físico. Agora sim: matricula.nota vira DECIMAL(4,1), aluno.nome vira VARCHAR(100) NOT NULL, matricula ganha UNIQUE(aluno_id, turma_id) pra ninguém se matricular duas vezes na mesma turma, e as FKs ganham regra de deleção.

2.2 O mesmo padrão em pedidos de e-commerce

Outro cenário clássico, porque ele ensina uma decisão que cai em prova: pedido e produto são N:N (um pedido tem vários produtos, um produto aparece em vários pedidos), então nasce item_pedido(pedido_id, produto_id, quantidade, preco_unitario). Repara no preco_unitario dentro do item: o preço do produto muda com o tempo, mas o pedido precisa lembrar quanto custava na hora da compra. Copiar o preço ali não é erro de normalização, é snapshot histórico de propósito (a gente volta nisso na seção 5.5).

🛠️Gambiarra boa

🛠️ Gambiarra boa: os primeiros 10 minutos do Módulo B são de papel. Conceitual no rascunho: caixas, linhas, cardinalidades. Só depois abre o MySQL. Quem modela direto no SQL descobre a tabela faltando no meio do CREATE e refaz tudo. Dez minutos de papel economizam trinta de retrabalho, e o rascunho ainda vira roteiro da apresentação ("essa foi minha modelagem inicial").

⚠️ Pega-ratão: pular o nível conceitual "porque o sistema é pequeno". O sistema da prova nunca é tão pequeno quanto parece: o enunciado esconde entidades (o RSVP esconde um status com data, o check-in esconde um registro com hora). O conceitual é onde essas coisas aparecem antes de custar caro.


3. Entidade, atributo, relacionamento e cardinalidade: a leitura fina

3.1 Achar entidades num enunciado

Entidade é uma coisa sobre a qual o sistema guarda informação com identidade própria. O truque de leitura: substantivos que o sistema precisa lembrar depois. No Wedding Pass: usuário, casamento, convidado, convite, mesa, check-in. "Relatório" não é entidade (é uma consulta). "Status" sozinho não é entidade (é atributo de alguém).

3.2 Tipos de atributo (e uma regra de ouro)

3.3 Cardinalidade com mínimo e máximo

Além do 1:1, 1:N e N:N, o modelo fino marca o mínimo: a relação é obrigatória ou opcional?

Essa leitura de mínimo é o que decide, lá no físico, se a FK é NOT NULL ou não. E é ela que evita a armadilha da obrigatoriedade dos dois lados: se "todo departamento tem ao menos um funcionário" e "todo funcionário tem departamento", como é que se insere o primeiro registro num banco vazio? (Segura essa pergunta: ela é a porta de entrada da ligação circular, seção 6.)

3.4 Resolvendo cada cardinalidade

⚠️Pega-ratão

⚠️ Pega-ratão: resolver N:N com lista dentro de campo ("João, Maria, Ana" numa coluna). Quebra busca, quebra contagem, quebra integridade, quebra 1FN. N:N pede tabela associativa, sempre. Se você se pegar querendo guardar uma lista numa coluna, tá faltando uma tabela.

🎯 Isso é a habilidade da Prova Surpresa. O Módulo A dá um enunciado desconhecido e 2h. Quem tem o método (substantivos → entidades → cardinalidades → FK no lado certo) modela qualquer domínio em minutos. O módulo 07 transforma isso num passo a passo completo (enunciado → entidades → endpoints → telas).


4. Chaves: o sistema de identidade do banco

🎯Foco de prova

🎯 Foco de prova: a banca verifica objetivamente se as tabelas estão de fato relacionadas por FK no banco, ou só "de mentira" (colunas soltas com nome parecido). FOREIGN KEY declarado é sim/não no CIS. E a escolha surrogate + natural como UNIQUE é um "porquê" pronto pra apresentação.


5. Normalização: 1FN, 2FN e 3FN com a tabela errada na mesa

Normalizar é eliminar redundância e dependência mal colocada. A teoria fica clara quando se parte do erro. Eis a "tabelona" que todo mundo já fez na vida (uma planilha virada tabela):

convidados_geral
| id | convidado      | telefones           | casamento     | data_casamento | mesa | capacidade_mesa |
| 1  | Ana Souza      | 5199..., 5198...    | Júlia & Pedro | 2026-11-21     | 5    | 8               |
| 2  | Bruno Lima     | 5197...             | Júlia & Pedro | 2026-11-21     | 5    | 8               |
| 3  | Carla Dias     | 5196..., 5195...    | Júlia & Pedro | 2026-11-21     | 2    | 10              |

5.1 As três anomalias que essa tabela cria

Normalização é o antídoto dessas três.

5.2 1FN: valores atômicos, sem grupos repetidos

Cada célula guarda um valor. telefones com dois números viola. E a variação "resolvida" com telefone1, telefone2, telefone3 também viola (grupo repetido: e o quarto telefone?).

Correção: telefone vira tabela filha.

CREATE TABLE telefone (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  convidado_id INT UNSIGNED NOT NULL,
  numero VARCHAR(15) NOT NULL,
  FOREIGN KEY (convidado_id) REFERENCES convidado(id)
);

(Na prova, se o enunciado pede um telefone por convidado, uma coluna resolve. 1FN pega quando são vários.)

5.3 2FN: todo campo depende da chave inteira

Só faz sentido com chave composta. Exemplo: tabela de presença com PK (convidado_id, evento_id):

presenca
| convidado_id | evento_id | nome_convidado | status     |

status depende do par completo (aquele convidado naquele evento): ok. nome_convidado depende só de convidado_id: violação de 2FN. O nome já mora na tabela convidado; aqui é cópia que vai dessincronizar.

Correção: tirar nome_convidado e buscar por JOIN quando precisar.

5.4 3FN: nenhum campo depende de outro campo não chave

Na tabelona: capacidade_mesa depende de mesa, e mesa não é chave. Isso é dependência transitiva (id → mesa → capacidade). Resultado: a capacidade da mesa 5 repetida em toda linha de convidado da mesa 5, e a anomalia de exclusão da Carla.

Correção: mesa vira tabela própria (id, numero, capacidade), convidado ganha mesa_id.

5.5 O resultado normalizado (e quando parar)

casamento(id, nome, data_evento, ...)
mesa(id, casamento_id, numero, capacidade)
convidado(id, casamento_id, mesa_id NULL, nome, ...)
telefone(id, convidado_id, numero)

Cada informação mora num lugar só. As três anomalias morreram.

Desnormalizar de propósito é ferramenta de gente grande, não desculpa de preguiça: o preco_unitario no item do pedido (snapshot histórico) e contadores cacheados em sistemas de altíssimo volume são casos legítimos. A diferença entre desnormalização e erro é uma só: você sabe dizer por quê.

🛠️Gambiarra boa

🛠️ Gambiarra boa (a regra de bolso). Na hora da prova ninguém recita definição de forma normal. O instinto que elas geram é o que se aplica: cada informação mora num lugar só. Se você está copiando o mesmo dado em várias linhas, falta uma tabela. Se um campo não fala sobre a "coisa" daquela tabela, ele está no lugar errado. Mas a definição precisa estar na cabeça também, porque banca pergunta ("por que você separou a mesa?" → "dependência transitiva, terceira forma normal" é resposta de nível 3).

⚠️ Pega-ratão do outro extremo: normalizar demais. Vinte tabelas microscópicas numa prova de 2h30 atrasam o CREATE, complicam cada JOIN e não pontuam mais por isso. 3FN com bom senso é o equilíbrio: organizado e prático.


6. Ligação circular: o que é, quando é problema e como resolver

Ligação circular (referência circular) é quando as FKs formam um ciclo: seguindo as setas de dependência, você volta pro ponto de partida. Existem três formas, e elas têm veredictos diferentes. Isso importa porque a pergunta "isso é errado?" não tem uma resposta só: tem três.

6.1 Auto-relacionamento (a tabela aponta pra ela mesma): legítimo e elegante

CREATE TABLE funcionario (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(100) NOT NULL,
  gerente_id INT UNSIGNED NULL,
  FOREIGN KEY (gerente_id) REFERENCES funcionario(id)
);

Todo funcionário pode ter um gerente, que também é funcionário. Isso modela hierarquia com uma tabela só: organograma, categoria com subcategoria, comentário com resposta, menu com submenu. Não é erro nenhum; é padrão clássico.

Os dois cuidados que fazem ele funcionar:

  1. A FK aceita NULL. Alguém está no topo (a diretora não tem gerente). Sem NULL permitido, ninguém consegue ser o primeiro inserido.
  2. Consultar hierarquia profunda exige recursão. "Quem responde pra Ana, direta e indiretamente?" não sai com um JOIN simples. Um nível sai com self-join (seção 14); a árvore inteira pede CTE recursiva (WITH RECURSIVE, que o MySQL 8 tem). Na prova, quase sempre um nível basta.

6.2 Ciclo entre duas tabelas (A → B e B → A): sinal amarelo, resolvível

O caso clássico:

-- departamento tem um chefe (que é funcionário)
-- funcionário pertence a um departamento
departamento(id, nome, chefe_id → funcionario.id)
funcionario(id, nome, departamento_id → departamento.id)

Aqui nasce o problema do ovo e da galinha: pra inserir o departamento preciso do chefe, pra inserir o chefe preciso do departamento. Com as duas FKs NOT NULL, o banco vazio trava: nenhum insert é possível. E o espelho disso acontece no DELETE: com RESTRICT dos dois lados, nenhuma das duas linhas consegue ser apagada primeiro.

Como resolver (em ordem de preferência):

  1. Quebrar o ciclo deixando uma FK aceitar NULL. departamento.chefe_id NULL: cria o departamento sem chefe, insere o funcionário, faz o UPDATE apontando o chefe. Duas etapas, problema resolvido. Escolhe pra ser anulável o lado cuja ausência temporária faz sentido no negócio (departamento sem chefe por uns dias é normal; funcionário sem departamento talvez não).
  2. Repensar se as duas FKs precisam existir. Muitas vezes uma delas é derivável ou modelável de outro jeito: um cargo na tabela de funcionário ("chefe") + a FK de departamento resolve sem ciclo nenhum. Ciclo que dá pra dissolver é ciclo que deveria ser dissolvido.
  3. Última instância (fora da prova): inserir os dois dentro de uma transação com verificação adiada. O MySQL/InnoDB não suporta constraint adiada (DEFERRABLE, coisa do PostgreSQL), então no MySQL essa porta nem existe. Mais um motivo pra resolver no modelo.

6.3 Ciclo longo (A → B → C → A): cheiro forte de modelagem errada

Quando o ciclo atravessa três ou mais tabelas, quase sempre alguma daquelas FKs não representa um relacionamento real, e sim uma tentativa de atalho ("vou guardar o casamento_id no check-in pra não fazer JOIN"). O check-in já chega no casamento via convidado; a FK extra é redundante, pode dessincronizar e ainda fecha o ciclo. Regra prática: cada fato do negócio entra no modelo uma vez. Se uma FK é derivável seguindo outras FKs, ela provavelmente não deveria existir.

6.4 No Wedding Pass

O modelo natural é acíclico: casamento ← mesa ← convidado ← convite / checkin, tudo apontando "pra cima" sem voltar. Um ciclo apareceria se, por exemplo, a mesa ganhasse um convidado_responsavel_id (mesa → convidado → mesa). Se o enunciado pedir "responsável pela mesa", a solução da seção 6.2 se aplica direto: convidado_responsavel_id NULL, preenchido depois que os convidados existem.

🎯Foco de prova

🎯 Resposta pronta pra banca (e pro Diogo 🙂): "ligação circular é errado?" Auto-relacionamento não é errado, é o padrão certo pra hierarquia. Ciclo entre tabelas é sinal amarelo: trava inserts e deletes, e se resolve quebrando o ciclo com uma FK anulável ou repensando o modelo. Ciclo longo quase sempre denuncia FK redundante. Saber essa resposta em três frases é diferencial de apresentação, porque a maioria só sabe dizer "acho que é ruim".

⚠️ Pega-ratão: obrigatoriedade nos dois lados do relacionamento (todo X tem Y e todo Y tem X, ambos NOT NULL). Mesmo sem FK circular declarada, isso cria o ovo-e-galinha na prática. Sempre pergunte: "com o banco vazio, qual linha entra primeiro?" Se a resposta é "nenhuma", o modelo tem um nó.


7. Integridade referencial: CASCADE, SET NULL e RESTRICT com casos reais

Integridade referencial é a garantia de que nenhuma FK aponta pro vazio. O banco cuida disso reagindo quando alguém apaga ou altera a linha "pai". A reação é você quem escolhe, na declaração da FK:

FOREIGN KEY (convidado_id) REFERENCES convidado(id)
  ON DELETE CASCADE
  ON UPDATE CASCADE
Regra Ao apagar o pai... Use quando o filho...
RESTRICT / NO ACTION O banco barra a exclusão Não pode perder o pai (registro histórico, financeiro)
CASCADE Os filhos somem junto Só existe em função do pai (convite sem convidado não significa nada)
SET NULL A FK do filho vira NULL Sobrevive sem o pai (a FK precisa aceitar NULL)

(No MySQL/InnoDB, RESTRICT e NO ACTION se comportam igual, e o padrão quando você não escreve nada é esse. ON UPDATE CASCADE cobre o caso raro de a PK do pai mudar; com AUTO_INCREMENT isso quase não acontece, mas declarar não custa.)

7.1 As decisões do Wedding Pass, uma a uma

FK Regra Porquê (esse é o texto da tua apresentação)
convite.convidado_id CASCADE Convite é papel do convidado. Apagou o convidado, o convite perde o sentido
convidado.mesa_id SET NULL Apagou a mesa, o convidado continua existindo, só fica sem lugar marcado
convidado.casamento_id CASCADE (ou RESTRICT) Apagar o casamento = limpar o evento inteiro (CASCADE); ou proibir apagar casamento com convidados (RESTRICT). As duas defendem; escolhe e justifica
checkin.convidado_id RESTRICT Check-in é registro de fato: aquela pessoa entrou. Apagar convidado que já entrou silenciosamente some com histórico. Barrar e exigir decisão explícita é o comportamento seguro
mesa.casamento_id CASCADE Mesa só existe dentro de um casamento

Repara que não existe UMA resposta certa pra tudo: existe resposta defendida. "Escolhi RESTRICT no check-in porque é registro histórico" é frase de nível 3.

⚠️Pega-ratão

⚠️ Pega-ratão: o CASCADE em cadeia. casamento CASCADE→ convidado CASCADE→ convite/checkin: um DELETE FROM casamento WHERE id = 1 varre o evento inteiro em silêncio. Às vezes é exatamente o que se quer; às vezes é meio banco sumindo por um clique. Antes de pôr CASCADE, siga a cadeia até o fim e pergunte "tudo isso deve sumir junto?". E no front, ação com CASCADE embaixo SEMPRE pede confirmação destrutiva (módulo 08).

🎯 Foco de prova: o descritivo cita integridade referencial explicitamente, e ela é verificável de forma objetiva (a banca tenta apagar um pai com filhos e observa). Fazer o banco impedir estado inválido é mais forte que confiar que a aplicação nunca erra. Essa frase também é da apresentação.


8. Constraints: o banco dizendo "não" (defesa em profundidade)

Além das chaves, o banco impõe regras nos campos. São a última linha de defesa: mesmo que a API tenha bug, mesmo que alguém insira na mão, a regra segura.

CREATE TABLE mesa (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  casamento_id INT UNSIGNED NOT NULL,          -- NOT NULL: obrigatório
  numero INT UNSIGNED NOT NULL,
  capacidade INT UNSIGNED NOT NULL,
  CONSTRAINT chk_capacidade CHECK (capacidade BETWEEN 1 AND 50),  -- CHECK: regra de valor
  CONSTRAINT uq_mesa_numero UNIQUE (casamento_id, numero),        -- UNIQUE composto
  FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE
);

O UNIQUE que ganha jogo: UNIQUE (convidado_id) na tabela checkin é o bloqueio de dupla entrada garantido no nível mais baixo. Nem bug de API, nem dois cliques rápidos, nem duas abas abertas furam. O módulo 07 constrói a feature completa em cima dessa constraint.

🛠️Gambiarra boa

🛠️ Gambiarra boa: deixa o banco trabalhar por você. Toda regra que vira constraint é regra que nunca será furada e que a aplicação valida "de graça" (a API só precisa traduzir o erro do banco pra mensagem bonita). Constraint é a defesa mais barata e mais confiável do sistema inteiro. A validação da API (módulo 04) existe pra dar mensagem boa; a constraint existe pra garantir a verdade.


9. Tipos de dados MySQL: escolher certo é pontuar de graça

Tipo errado funciona no dia 1 e explode no dia 30 (ou na frente da banca). A tabela de decisão:

Dado Tipo certo Porquê Erro comum
id INT UNSIGNED AUTO_INCREMENT 4 bytes, até ~4,29 bi sem sinal; sobra pra prova BIGINT à toa (dobra o espaço de todo índice)
Nome, e-mail VARCHAR(100) / VARCHAR(150) Tamanho variável, só ocupa o que usa TEXT pra campo curto (não pode ter DEFAULT, índice pior)
CPF, telefone, CEP VARCHAR Tem zero à esquerda e ninguém soma CPF INT (come o zero à esquerda: CPF 012... vira 12...)
Dinheiro DECIMAL(10,2) Exato. 0.10 é 0.10 FLOAT/DOUBLE: binário não representa 0.1 direito, e centavos somem em soma grande
Data do evento DATE Só a data, compara e calcula VARCHAR "21/11/2026" (não ordena, não entra em DATEDIFF)
Momento do check-in DATETIME Data + hora, range gigante —
criado_em / atualizado_em DATETIME DEFAULT CURRENT_TIMESTAMP (e ON UPDATE CURRENT_TIMESTAMP no segundo) Auditoria de graça Preencher na aplicação e esquecer em algum INSERT
Status ENUM('pendente','confirmado','recusado') Legível, compacto, valida sozinho VARCHAR livre (aceita "confirmadu")
Sim/não TINYINT(1) (o BOOLEAN do MySQL) Padrão da casa VARCHAR "sim"/"não"
Texto longo (observações) TEXT Até 64KB —

DATETIME vs TIMESTAMP, porque banca gosta: os dois guardam data+hora. TIMESTAMP converte pro fuso do servidor e só vai até janeiro de 2038 (4 bytes com época Unix); DATETIME guarda literal o que você mandou e vai do ano 1000 ao 9999. Regra prática do nosso contexto: DATETIME em tudo, com DEFAULT CURRENT_TIMESTAMP onde precisar de carimbo automático. Simples e sem surpresa de fuso.

ENUM vs tabela de domínio, a decisão honesta: ENUM é perfeito quando a lista é pequena e estável (status de convite não ganha valores novos toda semana). Se a lista cresce ou tem dados próprios (categorias com descrição e cor), aí é tabela + FK. Na prova, status é ENUM e segue o jogo; saber verbalizar o trade-off é o bônus.

⚠️Pega-ratão

⚠️ Pega-ratão: FLOAT pra dinheiro. É o erro de tipo mais clássico que existe, e é dos que a banca conhece de cor. DECIMAL(10,2) custa o mesmo pra escrever e é a resposta certa em qualquer sistema com valor monetário. Grava como reflexo.


10. O script do banco: DDL completo do Wedding Pass

Tudo que veio até aqui, materializado. Esse script é referência de estudo e esqueleto pra treinar (a prova terá outro domínio, mas o formato é esse):

DROP DATABASE IF EXISTS wedding_pass;
CREATE DATABASE wedding_pass CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE wedding_pass;

-- Ordem de criação: pais antes de filhos (a FK exige que o alvo exista)

CREATE TABLE usuario (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(100) NOT NULL,
  email VARCHAR(150) NOT NULL,
  senha_hash VARCHAR(255) NOT NULL,            -- hash bcrypt, NUNCA a senha (seção 18.3)
  perfil ENUM('admin','organizador','recepcao') NOT NULL DEFAULT 'recepcao',
  criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_usuario_email UNIQUE (email)
) ENGINE=InnoDB;

CREATE TABLE casamento (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  nome VARCHAR(150) NOT NULL,                  -- "Júlia & Pedro"
  data_evento DATETIME NOT NULL,
  local VARCHAR(200) NOT NULL,
  capacidade_total INT UNSIGNED NOT NULL,
  criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT chk_capacidade_total CHECK (capacidade_total > 0)
) ENGINE=InnoDB;

CREATE TABLE mesa (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  casamento_id INT UNSIGNED NOT NULL,
  numero INT UNSIGNED NOT NULL,
  capacidade INT UNSIGNED NOT NULL,
  CONSTRAINT chk_mesa_capacidade CHECK (capacidade BETWEEN 1 AND 50),
  CONSTRAINT uq_mesa_numero UNIQUE (casamento_id, numero),
  FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE
) ENGINE=InnoDB;

CREATE TABLE convidado (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  casamento_id INT UNSIGNED NOT NULL,
  mesa_id INT UNSIGNED NULL,                   -- (0,1): pode não ter mesa ainda
  nome VARCHAR(100) NOT NULL,
  email VARCHAR(150) NULL,
  telefone VARCHAR(15) NULL,
  acompanhantes TINYINT UNSIGNED NOT NULL DEFAULT 0,
  criado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT chk_acompanhantes CHECK (acompanhantes <= 5),
  FOREIGN KEY (casamento_id) REFERENCES casamento(id) ON DELETE CASCADE,
  FOREIGN KEY (mesa_id) REFERENCES mesa(id) ON DELETE SET NULL
) ENGINE=InnoDB;

CREATE TABLE convite (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  convidado_id INT UNSIGNED NOT NULL,
  codigo CHAR(8) NOT NULL,                     -- código de negócio, aleatório, não sequencial
  status ENUM('pendente','confirmado','recusado') NOT NULL DEFAULT 'pendente',
  enviado_em DATETIME NULL,
  respondido_em DATETIME NULL,
  CONSTRAINT uq_convite_codigo UNIQUE (codigo),
  CONSTRAINT uq_convite_convidado UNIQUE (convidado_id),  -- um convite por convidado
  FOREIGN KEY (convidado_id) REFERENCES convidado(id) ON DELETE CASCADE
) ENGINE=InnoDB;

CREATE TABLE checkin (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  convidado_id INT UNSIGNED NOT NULL,
  registrado_por INT UNSIGNED NOT NULL,        -- qual usuário da recepção registrou
  registrado_em DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT uq_checkin_convidado UNIQUE (convidado_id),  -- 🔒 bloqueio de dupla entrada
  FOREIGN KEY (convidado_id) REFERENCES convidado(id) ON DELETE RESTRICT,
  FOREIGN KEY (registrado_por) REFERENCES usuario(id) ON DELETE RESTRICT
) ENGINE=InnoDB;

Anatomia das decisões (cada uma é um "porquê" de apresentação):

🛠️Gambiarra boa

🛠️ Gambiarra boa: um arquivo schema.sql versionado no Git desde o dia 1. Banco não se cria clicando no Workbench sem registro: se cria por script, que roda inteiro, do zero, quantas vezes precisar. O script é a memória, a evidência pra banca e o botão de pânico (deu ruim? dropa e roda de novo, 10 segundos).


11. Dicionário de dados: o mapa que acompanha o modelo

Dicionário de dados é a documentação tabular do banco: cada tabela, cada coluna, tipo, regra e significado. O plano de curso pede, e a banca reconhece na hora. Formato enxuto, exemplo pra convite:

Coluna Tipo Nulo? Regra Descrição
id INT UNSIGNED não PK, auto Identificador interno
convidado_id INT UNSIGNED não FK → convidado, UNIQUE, CASCADE Dono do convite (um por convidado)
codigo CHAR(8) não UNIQUE, gerado aleatório Código que o convidado usa no RSVP e check-in
status ENUM não pendente/confirmado/recusado, DEFAULT pendente Situação da resposta
enviado_em DATETIME sim — Quando o convite foi disparado
respondido_em DATETIME sim — Quando o convidado respondeu
🛠️Gambiarra boa

🛠️ Gambiarra boa: o dicionário de prova se escreve em 5 minutos DEPOIS que o schema.sql está pronto, praticamente transcrevendo o script pra tabela. Custo mínimo, cara de documentação profissional, e ainda organiza a tua própria cabeça pra apresentação. Se o tempo apertar demais, o script comentado (como o da seção 10) já cumpre meio papel.


12. Manipulação: INSERT, UPDATE e DELETE sem sustos

-- INSERT simples
INSERT INTO convidado (casamento_id, nome, email, telefone)
VALUES (1, 'Ana Souza', 'ana@email.com', '51999990000');

-- INSERT múltiplo (a base do seed): uma instrução, várias linhas
INSERT INTO mesa (casamento_id, numero, capacidade) VALUES
  (1, 1, 8), (1, 2, 8), (1, 3, 10), (1, 4, 6);

-- INSERT ... SELECT: inserir a partir de consulta (gerar convites pra quem não tem)
INSERT INTO convite (convidado_id, codigo)
SELECT c.id, UPPER(SUBSTRING(MD5(RAND()), 1, 8))
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id
WHERE cv.id IS NULL;

-- UPDATE: sempre com WHERE
UPDATE convite SET status = 'confirmado', respondido_em = NOW()
WHERE codigo = 'A3F9K2LM';

-- DELETE: sempre com WHERE
DELETE FROM convidado WHERE id = 42;
⚠️Pega-ratão

⚠️ Pega-ratão (o clássico dos clássicos): UPDATE/DELETE sem WHERE. Atualiza ou apaga a tabela INTEIRA, sem confirmação, sem volta. Na pressa da prova é um desastre real.

🛠️ Gambiarra boa: o ritual do SELECT antes. Antes de qualquer UPDATE/DELETE delicado, roda um SELECT * FROM tabela WHERE <mesma condição> e olha o que volta. É exatamente o conjunto que vai ser afetado. Conferiu, troca o SELECT * por UPDATE ... SET ou DELETE. Dez segundos que blindam contra o pior erro possível.


13. Consultas: SELECT do básico ao afiado

13.1 Filtros

SELECT nome, email FROM convidado
WHERE casamento_id = 1
  AND mesa_id IS NULL                         -- NULL se testa com IS, nunca com =
  AND nome LIKE 'A%'                          -- começa com A ('%A%' = contém)
  AND acompanhantes BETWEEN 1 AND 3
  AND id IN (SELECT convidado_id FROM convite WHERE status = 'pendente')
ORDER BY nome ASC
LIMIT 10 OFFSET 0;                            -- página 1 com 10 itens (paginação, módulo 04)

Pontos finos: NULL não é igual a nada, nem a outro NULL (WHERE mesa_id = NULL não acha NADA; é IS NULL). LIKE '%texto%' com curinga no começo não usa índice (seção 16), mas na escala da prova não dói. AND tem precedência sobre OR: misturou os dois, usa parênteses.

13.2 Funções que resolvem prova

SELECT
  CONCAT(nome, ' (mesa ', COALESCE(mesa_id, 'sem mesa'), ')') AS etiqueta,
  UPPER(nome) AS nome_maiusculo,
  COALESCE(email, 'sem e-mail') AS contato,       -- primeiro valor não nulo
  IF(acompanhantes > 0, 'com acompanhante', 'sozinho') AS tipo,
  CASE
    WHEN acompanhantes = 0 THEN 'individual'
    WHEN acompanhantes <= 2 THEN 'família pequena'
    ELSE 'família grande'
  END AS faixa
FROM convidado;

13.3 Datas: o grupo de função que mais cai

SELECT
  NOW(),                                         -- data e hora agora
  CURDATE(),                                     -- só a data de hoje
  DATEDIFF(ca.data_evento, CURDATE()) AS dias_faltando,
  DATE_FORMAT(ca.data_evento, '%d/%m/%Y %H:%i') AS data_br,
  DATE_ADD(ca.data_evento, INTERVAL -7 DAY) AS prazo_rsvp
FROM casamento ca;

-- Filtrar por período (check-ins da última hora)
SELECT * FROM checkin
WHERE registrado_em >= NOW() - INTERVAL 1 HOUR;

DATEDIFF devolve dias; pra outras unidades, TIMESTAMPDIFF(MINUTE, inicio, fim). DATE_FORMAT formata pra exibir (%d/%m/%Y é o formato BR), mas guarda-se SEMPRE no tipo de data nativo: formatação é saída, não armazenamento.


14. JOINs: onde o relacional mostra a que veio

JOIN junta linhas de tabelas relacionadas, casando pela condição do ON (quase sempre FK = PK).

-- INNER JOIN: só quem tem correspondência dos dois lados
-- "convidados COM convite, mostrando o status"
SELECT c.nome, cv.codigo, cv.status
FROM convidado c
INNER JOIN convite cv ON cv.convidado_id = c.id;

-- LEFT JOIN: todos da esquerda, com ou sem par na direita
-- "TODOS os convidados, e o convite de quem tiver"
SELECT c.nome, cv.status              -- cv.status vem NULL pra quem não tem convite
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id;

-- O padrão-ouro "o que está FALTANDO": LEFT JOIN + IS NULL
-- "convidados SEM convite" (é a query que alimentou o INSERT...SELECT da seção 12)
SELECT c.nome
FROM convidado c
LEFT JOIN convite cv ON cv.convidado_id = c.id
WHERE cv.id IS NULL;

-- Três tabelas (vai encadeando): convidado + mesa + status do convite
SELECT c.nome, m.numero AS mesa, cv.status
FROM convidado c
LEFT JOIN mesa m ON m.id = c.mesa_id
LEFT JOIN convite cv ON cv.convidado_id = c.id
ORDER BY m.numero, c.nome;

-- Self-join (o auto-relacionamento da seção 6.1 em ação)
SELECT f.nome AS funcionario, g.nome AS gerente
FROM funcionario f
LEFT JOIN funcionario g ON g.id = f.gerente_id;

Guia rápido de escolha: INNER quando só interessa quem tem par. LEFT quando a lista da esquerda precisa vir completa (lista de convidados com dados opcionais ao lado: quase sempre é LEFT). RIGHT existe, mas todo RIGHT vira um LEFT invertendo a ordem das tabelas; o mercado escreve LEFT e pronto. UNION empilha resultados de dois SELECTs com as mesmas colunas (UNION ALL mantém duplicados e é mais rápido; UNION deduplica).

⚠️Pega-ratão

⚠️ Pega-ratão: esquecer a condição do JOIN (ou errar ela). Sem ON correto o banco cruza todo mundo com todo mundo (produto cartesiano): 50 convidados × 50 convites = 2500 linhas de lixo. Se uma consulta devolver muito mais linha que o esperado, o primeiro suspeito é o ON.

🛠️ Gambiarra boa: apelido curto pra toda tabela (convidado c, convite cv) e coluna sempre prefixada (c.nome). Em JOIN de 3+ tabelas com colunas de nome igual (id, nome), isso evita o erro "ambiguous column" e deixa a query legível pra banca.


15. Agrupamento e agregação: o SQL que vira dashboard

Funções de agregação resumem linhas: COUNT, SUM, AVG, MIN, MAX. Com GROUP BY, o resumo sai por grupo. E aqui mora o dashboard inteiro do Wedding Pass:

-- Os números do topo do dashboard
SELECT
  COUNT(*) AS total_convidados,
  SUM(acompanhantes) AS total_acompanhantes,
  COUNT(*) + SUM(acompanhantes) AS pessoas_esperadas
FROM convidado WHERE casamento_id = 1;

-- Convites por status (o gráfico de pizza)
SELECT cv.status, COUNT(*) AS quantidade
FROM convite cv
JOIN convidado c ON c.id = cv.convidado_id
WHERE c.casamento_id = 1
GROUP BY cv.status;

-- Ocupação por mesa (COUNT(c.id), não COUNT(*): mesa vazia conta 0)
SELECT m.numero, m.capacidade,
       COUNT(c.id) AS ocupacao,
       ROUND(COUNT(c.id) / m.capacidade * 100) AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
WHERE m.casamento_id = 1
GROUP BY m.id, m.numero, m.capacidade;

-- Só as mesas acima de 90% (o ALERTA do enunciado): HAVING filtra o grupo
SELECT m.numero, COUNT(c.id) / m.capacidade * 100 AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
GROUP BY m.id, m.numero, m.capacidade
HAVING pct >= 90;

-- Lotação em tempo real: quem já entrou (check-ins) sobre a capacidade
SELECT COUNT(ck.id) AS presentes, ca.capacidade_total,
       ROUND(COUNT(ck.id) / ca.capacidade_total * 100, 1) AS lotacao_pct
FROM casamento ca
LEFT JOIN convidado c ON c.casamento_id = ca.id
LEFT JOIN checkin ck ON ck.convidado_id = c.id
WHERE ca.id = 1
GROUP BY ca.id, ca.capacidade_total;

-- Check-ins por hora (o gráfico de linha da festa)
SELECT DATE_FORMAT(registrado_em, '%H:00') AS hora, COUNT(*) AS entradas
FROM checkin
GROUP BY hora
ORDER BY hora;

WHERE vs HAVING, de uma vez por todas: WHERE filtra linhas antes de agrupar; HAVING filtra grupos depois de agregar. "Convidados do casamento 1" é WHERE; "mesas com mais de 90%" é HAVING (porque o % só existe depois do COUNT). E a regra do GROUP BY: toda coluna do SELECT que não está dentro de função de agregação precisa estar no GROUP BY (o MySQL 8 reclama se não estiver, e ele está certo).

Subquery quando o filtro depende de outro resultado: WHERE acompanhantes > (SELECT AVG(acompanhantes) FROM convidado) (convidados acima da média). Subquery no FROM cria uma "tabela temporária" pra agregar em cima de agregação. São ferramentas de canivete: use quando o JOIN não expressa a pergunta.

🎯Foco de prova

🎯 O dashboard nasce no SQL, e essa seção É o dashboard. Cada query acima corresponde a um número ou gráfico da tela do módulo 07. Quem sai daqui com essas seis queries no músculo monta o dashboard do Módulo C em minutos, porque a parte difícil (a pergunta certa em SQL) já está pronta. O front só pinta o resultado.


16. Views e índices: o tunning que a UC3 pede

16.1 View: consulta com nome

View é uma consulta salva que se usa como tabela:

CREATE VIEW vw_ocupacao_mesa AS
SELECT m.id, m.casamento_id, m.numero, m.capacidade,
       COUNT(c.id) AS ocupacao,
       ROUND(COUNT(c.id) / m.capacidade * 100) AS pct
FROM mesa m
LEFT JOIN convidado c ON c.mesa_id = m.id
GROUP BY m.id, m.casamento_id, m.numero, m.capacidade;

-- E agora o dashboard vira consulta simples:
SELECT * FROM vw_ocupacao_mesa WHERE casamento_id = 1 AND pct >= 90;

O que a view compra: a query cabeluda mora num lugar só (mudou a regra, muda na view, todo consumidor ganha de graça), e o código da API fica limpo. O que ela NÃO compra: velocidade (a view roda a consulta por baixo toda vez; não é cache). Na prova, view no dashboard é organização que a banca vê e a UC3 pede nominalmente.

16.2 Índice: o atalho de leitura

Sem índice, buscar WHERE email = 'ana@...' obriga o banco a ler a tabela INTEIRA (full scan). Índice é uma estrutura ordenada ao lado da tabela (uma árvore B-tree) que leva direto às linhas certas, como o índice remissivo de um livro.

O que já vem indexado sem você pedir: PK (sempre), UNIQUE (sempre) e, no InnoDB, toda FK declarada ganha índice automaticamente. Ou seja: o schema da seção 10 já nasce bem indexado pros JOINs.

Quando criar na mão: coluna que aparece com frequência em WHERE, ORDER BY ou JOIN e que não é PK/UNIQUE/FK. No Wedding Pass, o candidato real é a busca por nome:

CREATE INDEX idx_convidado_nome ON convidado(nome);

Índice composto (mais de uma coluna) segue a regra da esquerda: um índice em (casamento_id, nome) serve pra filtrar por casamento_id sozinho e por casamento_id + nome, mas NÃO serve pra filtrar só por nome (o MySQL lê o índice da esquerda pra direita e não pula coluna). Por isso a ordem das colunas importa: a coluna de igualdade mais usada vai primeiro.

O custo que ninguém conta: todo índice desacelera escrita (cada INSERT/UPDATE atualiza a tabela E cada índice) e ocupa espaço. Índice em tudo é tão errado quanto índice em nada.

E a ferramenta de conferência: EXPLAIN SELECT ... mostra o plano do banco. Na coluna type, ALL significa full scan (sem índice); ref/range significa que o índice entrou. Não precisa dominar EXPLAIN pra prova; precisa saber que ele existe e o que ALL denuncia.

🛠️Gambiarra boa

🛠️ Gambiarra boa (a política de índice da prova): confia nos automáticos (PK, UNIQUE, FK) e adiciona no máximo um ou dois na mão, nas colunas de busca do usuário (nome, código). Nessa escala de dados a diferença de performance é invisível, mas a frase "criei índice no nome porque é o campo de busca da recepção" na apresentação mostra que você conhece a ferramenta e o critério. É dos pontos de tunning mais baratos do CIS.


17. Transações: tudo ou nada

Transação agrupa operações numa unidade atômica (o A do ACID): ou tudo confirma, ou tudo desfaz.

START TRANSACTION;

INSERT INTO convidado (casamento_id, nome, email)
VALUES (1, 'Duda Reis', 'duda@email.com');

INSERT INTO convite (convidado_id, codigo)
VALUES (LAST_INSERT_ID(), 'K2M8XP4Q');   -- LAST_INSERT_ID(): o id do insert acima

COMMIT;      -- confirma os dois juntos
-- ROLLBACK; -- ou desfaz os dois juntos

Sem transação, se o segundo INSERT falhar (código duplicado, por exemplo), o convidado fica criado sem convite: estado meio-termo que ninguém pediu. Com transação, falhou qualquer parte → ROLLBACK → banco como se nada tivesse acontecido.

Quando usar na prova: toda operação que escreve em mais de uma tabela e precisa ser coerente. Criar convidado + convite. Registrar check-in + qualquer atualização de contagem. No módulo 04 a transação reaparece dentro do código da API (nas duas stacks), e no módulo 07 ela se junta com o UNIQUE do check-in pra fechar a solução da condição de corrida (dois check-ins simultâneos do mesmo código: o isolamento + a constraint garantem que só um passa).

🎯Foco de prova

🎯 Vocabulário de nível 3: "essa operação é transacional porque escreve em duas tabelas" é o tipo de frase que, dita naturalmente na apresentação, vale mais que dez minutos de tela bonita. Banca técnica reconhece na hora quem entende atomicidade.


18. Backup, restore, import e export: a política de recuperação

18.1 Dump e restore (a dupla que salva provas)

# Backup (export): o banco inteiro vira um arquivo .sql com CREATEs e INSERTs
mysqldump -u root -p wedding_pass > backup_wedding_2026-08-14.sql

# Restore (import): o arquivo recria tudo
mysql -u root -p wedding_pass < backup_wedding_2026-08-14.sql

O mysqldump gera um script SQL completo: estrutura + dados. Serve de backup, de transporte entre máquinas (a dupla treina em casa e restaura no Senac) e de entrega (se a prova pedir "exporte o banco", é isso).

18.2 A política de recuperação (o conceito que a UC3 pede)

Política de recuperação responde três perguntas: o que copiar (o banco da prova), quando copiar (nos marcos: terminou o schema, terminou o seed, terminou uma feature grande) e como voltar (restore testado, não só backup guardado). Backup que nunca foi restaurado é promessa, não política.

Na competição, a política é simples e real: mysqldump ao fechar cada etapa do Módulo B + o schema.sql + seed.sql versionados no Git. Qualquer desastre (drop errado, UPDATE sem WHERE, máquina travou) se resolve em menos de um minuto.

Import/export de dados avulsos: SELECT ... INTO OUTFILE exporta CSV, LOAD DATA INFILE importa CSV em massa (é a "carga massiva" citada na UC9). Bom saber que existem; na prova, o caminho normal é o dump.

🛠️Gambiarra boa

🛠️ Gambiarra boa: o dump é a máquina do tempo. Vai fazer uma mexida grande e arriscada no banco no meio da prova? 15 segundos: mysqldump antes. Deu ruim? Restaura e ninguém soube. Essa disciplina tira o medo de mexer, e quem não tem medo de mexer trabalha mais rápido. (E o commit no Git faz o mesmo pelo código: módulo 02.)

18.3 Senha no banco: hash, nunca texto

🎯Foco de prova

🎯 Avaliação objetiva direta (sim/não): a banca abre a tabela de usuários e olha. Ou tem hash, ou é zero nesse item. E é zero dos dolorosos, porque custava duas linhas de código.

⚠️ Pega-ratão: senha em texto puro "só pra testar" e esquecer de trocar. Na correria acontece. A vacina: o seed já cria o usuário com a senha hasheada desde o primeiro dia, e ninguém nunca insere usuário na mão.


19. Seed: povoar o banco com massa realista

Seed é o script que carrega os dados iniciais, de forma automatizada e repetível. O descritivo pede, e a demo depende dele.

O que um seed de nível 3 tem:

-- seed.sql (trecho): multi-row INSERT é a forma
INSERT INTO usuario (nome, email, senha_hash, perfil) VALUES
  ('Admin',   'admin@wp.com',   '$2b$10$hash...aqui', 'admin'),
  ('Marina',  'marina@wp.com',  '$2b$10$hash...aqui', 'organizador'),
  ('Portaria','porta@wp.com',   '$2b$10$hash...aqui', 'recepcao');

INSERT INTO convidado (casamento_id, mesa_id, nome, email, acompanhantes) VALUES
  (1, 1, 'Ana Souza',  'ana@email.com',  1),
  (1, 1, 'Bruno Lima', 'bruno@email.com',0),
  (1, NULL, 'Carla Dias','carla@email.com',2),
  -- ... 30+ linhas, estados variados
  (1, 3, 'Zeca Prado', 'zeca@email.com', 0);

Pra gerar massa grande sem digitar: o INSERT ... SELECT da seção 12 gera os convites de todos de uma vez, e um loop pequeno na aplicação (ou uma planilha que monta as linhas de INSERT com fórmula de CONCAT) gera os convidados. Essa é a "carga massiva" da UC9 no nosso tamanho.

🛠️Gambiarra boa

🛠️ Gambiarra boa: seed é o palco pronto. Chega no Módulo C com o banco cheio e variado, e o dashboard nasce com números de verdade na primeira renderização. Chega no Módulo D (apresentação) com uma história pra contar em cima de dados que parecem reais. Seed bom se prepara no Módulo B pensando na demo do D: é o mesmo arquivo trabalhando três módulos.


20. Autoavaliação do módulo

  1. Quando um banco não relacional seria melhor escolha que o relacional? E por que o sistema da prova é o caso oposto?
  2. Explica a diferença entre modelo conceitual, lógico e físico, e o que acontece com um N:N em cada nível.
  3. No sistema da escola, por que a nota é atributo da matrícula e não do aluno? Acha um exemplo do mesmo padrão no Wedding Pass.
  4. Qual a diferença entre chave candidata, primária e substituta? Por que o CPF não deve ser PK?
  5. Monta de memória a "tabelona" errada e mostra qual forma normal cada problema dela viola.
  6. O que são as três anomalias (inserção, atualização, exclusão)? Dá um exemplo de cada.
  7. Ligação circular: quais os três tipos, qual deles é legítimo e como se quebra um ciclo entre duas tabelas?
  8. Com o banco vazio, que pergunta você faz pra detectar um nó de obrigatoriedade mútua no modelo?
  9. Justifica a regra de deleção de cada FK do Wedding Pass (CASCADE, SET NULL ou RESTRICT) como se estivesse na apresentação.
  10. Por que UNIQUE (convidado_id) na tabela de check-in é o bloqueio de dupla entrada mais confiável do sistema?
  11. Por que dinheiro é DECIMAL e nunca FLOAT? E por que CPF é VARCHAR e nunca INT?
  12. Qual a diferença entre WHERE e HAVING? Escreve a query das mesas acima de 90%.
  13. Escreve, sem olhar, a query "convidados sem convite" (LEFT JOIN + IS NULL) e explica por que INNER JOIN não resolveria.
  14. O que uma view compra e o que ela não compra? E qual a regra da esquerda do índice composto?
  15. Quando uma operação precisa de transação? Dá dois exemplos do Wedding Pass.
  16. Descreve a política de recuperação da prova: o que, quando e como voltar.
  17. Lista quatro características de um seed de nível 3.

Referências do módulo

Do plano de curso (UC3): - MySQL 8.0 Reference Manual — downloads.mysql.com/docs/refman-8.0-en.pdf (a fonte da verdade do dialeto da prova; treinar scanning nela é módulo 09 acontecendo) - ELMASRI, R.; NAVATHE, S. Sistemas de banco de dados. Pearson. (modelagem, formas normais e ACID a fundo) - TEOREY, T. et al. Projeto e modelagem de banco de dados. Elsevier. (o caminho conceitual → lógico → físico)

Do mercado: - Multiple-Column Indexes — MySQL Reference Manual (a regra da esquerda, da fonte) - Composite Indexes — MySQL for Developers, PlanetScale (curso gratuito, o melhor material prático de índice que existe) - Modelagem de Bancos Relacionais: boas práticas — DevMedia (inclui a armadilha da obrigatoriedade dos dois lados) - Modelagem de Bancos de Dados sem Segredos — Albert Eije, Medium (série em português, boa pra revisão)

💡Nota

Fundação de dados fechada, e fechada com porquês. Agora a gente constrói a camada que serve esses dados pro mundo, nas duas stacks: 04 — Back-end e API RESTful.

Competições Senac RS · Seletiva 26

Módulo 04

Módulo 04 — Back-end e API RESTful

BaseUC13 (Desenvolver back-end web, 96h)Peso na prova20% (back-end web + banco)NaturezaRevisão
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. É o Módulo B da prova (2h30, 1º dia), a metade "servidor" do sistema. O banco (módulo 03) guarda os dados; a API é quem serve esses dados pro mundo e guarda as regras de negócio. Vale 20%, e sustenta o front (40%): um front lindo consumindo uma API quebrada não pontua. Natureza 🔄 revisão, as duas já fizeram API na Regional. O jogo agora é fazer uma API limpa, segura e correta que chegue no nível 3.

Como ler o código deste módulo: o conceito é sempre um só, mas onde a sintaxe muda de verdade o exemplo aparece nas duas stacks: Node.js + Express + MySQL e PHP + MySQL (PHP puro, sem framework). O SQL por baixo é o mesmo dos dois lados, é o banco do módulo 03. No treino a gente domina uma stack como principal e conhece a outra o bastante pra não travar se o cenário mudar.


1. O que é uma API e por que ela existe

API é a camada que fica entre o mundo (o front, o app, outro sistema) e o banco. Ninguém fala com o banco direto: fala com a API, e ela decide o que pode e o que não pode.

1.1 Estático vs dinâmico: os dois tipos de conteúdo web

O plano de curso (UC13) pede essa distinção, e ela explica a arquitetura inteira do sistema:

O sistema da prova junta os dois: um front estático (módulo 05) que busca dados dinâmicos na API. A separação é limpa: arquivos de um lado, dados do outro.

1.2 Agnóstica ao cliente: a fonte única da verdade

O descritivo diz uma frase que vale ouro: a API tem que ser agnóstica ao cliente. Quer dizer: não importa quem está consumindo (o front web, um app mobile, o Postman), a regra é a mesma, porque ela mora no servidor. A API é a fonte única da verdade.

🎯Foco de prova

🎯 A consequência prática disso. Regra de negócio (não deixar check-in duplo, não passar da capacidade, só admin pode apagar) vive na API, nunca só no front. O front pode até esconder um botão, mas quem impede de verdade é o back. Se a regra só existe no front, qualquer um que chame a API direto fura ela. Isso é conceito de mercado e é foco de prova: a banca testa chamando a API por fora do front.

1.3 Anatomia de uma requisição e de uma resposta

Toda conversa com a API tem o mesmo formato. A requisição:

POST /convidados HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

{ "nome": "Marina Souza", "casamento_id": 1, "acompanhantes": 2 }

Quatro partes: o método (POST), a rota (/convidados), os cabeçalhos (o Content-Type avisa que o corpo é JSON; o Authorization carrega o crachá da seção 8) e o corpo (o JSON com os dados).

A resposta espelha:

HTTP/1.1 201 Created
Content-Type: application/json

{ "id": 42, "nome": "Marina Souza", "casamento_id": 1, "acompanhantes": 2 }

Três partes: o status code (201, seção 2.3), os cabeçalhos e o corpo. Ler requisição e resposta com naturalidade é o que faz o Postman (seção 15) e a aba Network do navegador virarem ferramentas de debug em vez de mistério.


2. REST: o jeito organizado de montar a API

REST é um conjunto de convenções pra deixar a API previsível. A ideia central: você trata tudo como recursos (coisas), e age sobre eles com verbos padronizados. Quem conhece a convenção adivinha a rota sem ler documentação. A banca conhece a convenção.

2.1 Recursos e rotas: substantivos no plural

Cada tipo de coisa é um recurso com um endereço. As regras que o mercado consolidou:

⚠️Pega-ratão

⚠️ Pega-ratão: misturar convenções (/convidado, /Convidados, /lista_convidados no mesmo sistema). Cada rota individual funciona, mas o conjunto vira bagunça, e consistência é exatamente o tipo de coisa que o julgamento (J) do CIS enxerga. Decide o padrão no minuto 1 e não desvia.

2.2 Verbos HTTP: a ação certa pra cada coisa

Verbo O que faz Exemplo no Wedding Pass Idempotente?
GET Buscar/listar (não muda nada) GET /convidados Sim
POST Criar novo POST /convidados Não (cada chamada cria outro)
PUT Substituir o recurso inteiro PUT /convidados/12 manda todos os campos Sim
PATCH Alterar parte do recurso PATCH /convidados/12 manda só { "mesa_id": 3 } Na prática, sim
DELETE Apagar DELETE /convidados/12 Sim

Idempotente quer dizer: chamar duas vezes dá no mesmo que chamar uma. DELETE /convidados/12 duas vezes deixa o sistema no mesmo estado (o segundo dá 404, mas nada muda). POST duas vezes cria dois registros. Essa palavra impressiona na apresentação e explica por que o check-in duplicado (seção 11.2) é um problema real: dois POSTs criam dois check-ins, a menos que alguém impeça.

PUT vs PATCH na prova: os dois atualizam. PUT formalmente substitui tudo, PATCH altera campos. Escolhe um pros seus updates e usa sempre o mesmo; consistência vale mais que a sutileza teórica.

🎯Foco de prova

🎯 Foco de prova: o descritivo cita "usar os verbos HTTP corretamente" de forma explícita. Isso é avaliação objetiva. Usar POST pra buscar dado, ou GET pra apagar, é o tipo de erro que a banca marca na hora. Verbo certo pra ação certa é ponto garantido, e é só disciplina. Regra prática: GET nunca muda nada no banco.

2.3 Status codes: a tabela completa da prova

Toda resposta vem com um código dizendo o que aconteceu. A família conta a história: 2xx deu certo, 4xx o cliente errou, 5xx o servidor errou. Os que o Wedding Pass usa:

Código Nome Quando usar
200 OK GET que achou, PUT/PATCH que atualizou
201 Created POST que criou (convidado novo, check-in registrado)
204 No Content DELETE que deu certo (sem corpo na resposta)
400 Bad Request dado inválido: nome vazio, e-mail torto, acompanhantes negativo
401 Unauthorized não autenticado: sem token, token vencido ou inválido
403 Forbidden autenticado mas sem permissão: recepção tentando DELETE
404 Not Found o recurso não existe: GET /convidados/9999
409 Conflict conflito com o estado atual: check-in duplicado, e-mail já cadastrado, mesa cheia
500 Internal Server Error o servidor quebrou (bug não tratado; a seção 12 esconde o detalhe)

A dupla que mais confunde: 401 vs 403. 401 é "eu não sei quem você é" (falta crachá). 403 é "eu sei quem você é, e você não pode" (crachá de recepção na porta da sala de admin). A banca adora perguntar essa diferença, e o front (módulo 05) trata os dois diferente: 401 manda pro login, 403 mostra "sem permissão".

Existe também o 422 (dado bem formado mas inválido pela regra), que parte do mercado usa no lugar do 400. Na prova, 400 pra dado inválido resolve tudo; de novo, consistência vale mais que sofisticação.

⚠️Pega-ratão

⚠️ Pega-ratão: responder tudo com 200, até quando deu erro. Aí o front não sabe se foi sucesso ou falha e trata errado. Pior ainda: 200 com { "sucesso": false } dentro. Status code certo é o que deixa o front reagir direito (mostrar o erro, redirecionar, avisar). É pouco esforço pra bastante clareza, e é lido na avaliação.

2.4 O mapa de endpoints do Wedding Pass

Desenhar esse mapa antes de codar é o primeiro passo do Módulo B (e a Prova Surpresa cobra exatamente essa habilidade, módulo 07). O esqueleto do sistema:

Método + rota O que faz Quem pode
POST /auth/login Autentica e devolve o token público
GET /convidados Lista com busca, filtro e paginação (seção 13) logado
GET /convidados/:id Detalhe de um convidado logado
POST /convidados Cadastra convidado admin, organizador
PUT /convidados/:id Atualiza convidado admin, organizador
DELETE /convidados/:id Remove convidado admin
GET /casamentos/:id/capacidade Ocupação atual + alerta 90% logado
GET /mesas · POST /mesas · etc. CRUD de mesas admin, organizador
POST /convidados/:id/checkin Check-in com bloqueio de dupla entrada recepcao, admin
GET /convites/:codigo Consulta pública do convite (RSVP) público
POST /convites/:codigo/resposta Confirma ou recusa presença público

Os perfis (admin, organizador, recepcao) são os do ENUM da tabela usuario do módulo 03. O fluxo completo de convite e RSVP ganha vida no módulo 07; aqui o que importa é enxergar o sistema inteiro como uma tabela dessas.

🛠️Gambiarra boa

🛠️ Gambiarra boa: na prova, escreve esse mapa num papel ou num comentário no topo do projeto nos primeiros 10 minutos do Módulo B. Ele vira seu checklist de progresso (riscar rota pronta é motivador e evita esquecer endpoint) e sua resposta pronta quando a banca perguntar "como você organizou a API?".


3. Arquitetura em camadas: código organizado nas duas stacks

O plano de curso pede código orientado a objetos (UC4), padrões de projeto e MVC (UC9). Na prática de API, isso vira camadas: cada arquivo com um trabalho só.

3.1 As três camadas (e a regra de ouro)

A regra de ouro: cada camada só fala com a vizinha de baixo. Controller chama serviço, serviço chama repositório. SQL dentro do controller é a camada pulando o muro, e é o primeiro sintoma de código que vai virar nota 1 em organização.

🛠️Gambiarra boa

🛠️ Gambiarra boa: separar rende nota e velocidade. Parece burocracia, mas separar em camadas é o que deixa o código legível pra banca (ponto de julgamento) e fácil de você mesma achar o bug na hora do desespero: erro de SQL? Repositório. Regra errada? Serviço. Status errado? Controller. Quando tudo está num arquivo de 400 linhas, achar onde mexer custa minutos que você não tem.

3.2 O esqueleto em Node.js + Express

api/
├── server.js                  ← sobe o Express, registra middlewares e rotas
├── .env                       ← segredos (NUNCA vai pro Git; módulo 02, .gitignore)
├── package.json
└── src/
    ├── config/
    │   └── db.js              ← pool de conexão (seção 4.1)
    ├── routes/
    │   └── convidados.routes.js    ← só mapeia URL → controller
    ├── controllers/
    │   └── convidados.controller.js
    ├── services/
    │   └── convidados.service.js   ← regras de negócio
    ├── repositories/
    │   └── convidados.repository.js ← SQL, só SQL
    ├── middlewares/
    │   ├── auth.js            ← exigeLogin, exigePerfil (seções 8 e 9)
    │   └── erros.js           ← tratador central (seção 12)
    └── helpers/
        └── validar.js         ← o validador reutilizável (seção 5.1)

O server.js que amarra tudo:

require('dotenv').config();
const express = require('express');
const cors = require('cors');

const app = express();
app.use(cors());            // seção 10
app.use(express.json());    // sem isso, req.body chega undefined

app.use('/auth', require('./src/routes/auth.routes'));
app.use('/convidados', require('./src/routes/convidados.routes'));
app.use('/mesas', require('./src/routes/mesas.routes'));

app.use(require('./src/middlewares/erros'));  // por último, sempre

app.listen(3000, () => console.log('API no ar na porta 3000'));

E um arquivo de rotas, que é só o mapa:

// src/routes/convidados.routes.js
const router = require('express').Router();
const controller = require('../controllers/convidados.controller');
const { exigeLogin, exigePerfil } = require('../middlewares/auth');

router.get('/', exigeLogin, controller.listar);
router.get('/:id', exigeLogin, controller.buscar);
router.post('/', exigeLogin, exigePerfil('admin', 'organizador'), controller.criar);
router.put('/:id', exigeLogin, exigePerfil('admin', 'organizador'), controller.atualizar);
router.delete('/:id', exigeLogin, exigePerfil('admin'), controller.remover);
router.post('/:id/checkin', exigeLogin, exigePerfil('recepcao', 'admin'), controller.checkin);

module.exports = router;

Repara que esse arquivo é o mapa de endpoints da seção 2.4 em código. A banca lendo ele entende o sistema inteiro em 20 segundos. Isso é organização pontuando.

⚠️Pega-ratão

⚠️ Pega-ratão do express.json(): esquecer essa linha faz todo req.body chegar undefined e a validação recusar tudo. Sintoma clássico: "mando o JSON certo no Postman e a API diz que o campo é obrigatório". Confere o middleware antes de caçar bug na validação.

3.3 O esqueleto em PHP puro

Sem framework, o padrão é o front controller: toda requisição entra por um único index.php, que roteia.

api/
├── public/
│   └── index.php              ← único ponto de entrada (roteador)
├── composer.json              ← dependências (firebase/php-jwt)
├── .env                       ← segredos (NUNCA vai pro Git)
└── src/
    ├── config/conexao.php     ← PDO (seção 4.2)
    ├── controllers/
    ├── services/
    ├── repositories/
    └── helpers/
        ├── validar.php
        ├── auth.php           ← exigeLogin, exigePerfil
        └── resposta.php       ← json() e erro() padronizados

O roteador em public/index.php:

<?php
require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../src/helpers/resposta.php';
// ... demais requires

header('Content-Type: application/json; charset=utf-8');

$metodo = $_SERVER['REQUEST_METHOD'];
$uri    = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

// "/convidados/12/checkin" → ['convidados', '12', 'checkin']
$partes  = explode('/', trim($uri, '/'));
$recurso = $partes[0] ?? '';
$id      = $partes[1] ?? null;
$acao    = $partes[2] ?? null;

$corpo = json_decode(file_get_contents('php://input'), true) ?? [];

try {
    match (true) {
        $metodo === 'POST'   && $recurso === 'auth' && $id === 'login' => login($corpo),
        $metodo === 'GET'    && $recurso === 'convidados' && !$id      => listarConvidados($_GET),
        $metodo === 'GET'    && $recurso === 'convidados'              => buscarConvidado($id),
        $metodo === 'POST'   && $recurso === 'convidados' && $acao === 'checkin' => fazerCheckin($id),
        $metodo === 'POST'   && $recurso === 'convidados'              => criarConvidado($corpo),
        $metodo === 'PUT'    && $recurso === 'convidados'              => atualizarConvidado($id, $corpo),
        $metodo === 'DELETE' && $recurso === 'convidados'              => removerConvidado($id),
        default => erro(404, 'Rota não encontrada'),
    };
} catch (Throwable $e) {
    error_log($e);                            // stack trace no log, não na resposta
    erro(500, 'Erro interno no servidor');    // seção 12
}

E os dois helpers de resposta que padronizam tudo:

<?php
// src/helpers/resposta.php
function json(int $status, array $dados): void {
    http_response_code($status);
    echo json_encode($dados, JSON_UNESCAPED_UNICODE);
    exit;
}

function erro(int $status, string $mensagem, array $detalhes = []): void {
    json($status, ['erro' => $mensagem, 'detalhes' => $detalhes]);
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: subir a API PHP em um comando. php -S localhost:8000 index.php dentro da pasta public/ sobe um servidor de desenvolvimento que roteia tudo pelo index.php. Sem Apache, sem XAMPP, sem virtual host. Pra prova é perfeito; e o servidor embutido recarrega o código a cada requisição, então nem restart precisa.

3.4 Uma requisição atravessando as camadas

POST /convidados com o JSON da Marina, do começo ao fim:

  1. Rota casa POST /convidados e vê que exige login + perfil (middlewares passam ou barram com 401/403).
  2. Controller valida o formato do corpo (seção 5.1). Formato ruim: responde 400 e acabou. Formato ok: chama o serviço.
  3. Serviço aplica a regra: a mesa 3 tem lugar pra Marina + 2 acompanhantes? Não tem: lança erro 409. Tem: chama o repositório.
  4. Repositório roda o INSERT com prepared statement (seção 6) e devolve o id novo.
  5. A resposta volta o caminho: serviço → controller → 201 Created com o convidado criado no corpo.

Essa narração de fluxo, com um exemplo concreto, é exatamente o que o Módulo D (apresentação, módulo 10) pede. Treinar ela agora é estudar pra duas provas ao mesmo tempo.


4. Conexão com o banco: o cabo entre a API e o MySQL

Primeira coisa que o Módulo B precisa funcionando. As duas stacks, lado a lado.

4.1 Node: mysql2 com pool de conexões

// src/config/db.js
const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  waitForConnections: true,
  connectionLimit: 10,
});

module.exports = pool;

Por que pool e não uma conexão só: o pool mantém um conjunto de conexões abertas e empresta uma a cada consulta. Sem pool, ou você abre e fecha conexão a cada query (lento) ou segura uma conexão global que cai e derruba a API junto. createPool resolve os dois problemas com o mesmo esforço de digitação que createConnection. Não tem motivo pra não usar.

Usando (repare no destructuring do array que o mysql2 devolve):

const pool = require('../config/db');
const [linhas] = await pool.query('SELECT * FROM convidado WHERE casamento_id = ?', [1]);

4.2 PHP: PDO

PDO é a interface padrão do PHP pra banco. As três opções do construtor abaixo não são decoração, são o que faz o PDO se comportar direito:

<?php
// src/config/conexao.php
function conectar(): PDO {
    static $pdo = null;
    if ($pdo === null) {
        $pdo = new PDO(
            'mysql:host=localhost;dbname=wedding_pass;charset=utf8mb4',
            getenv('DB_USER'),
            getenv('DB_PASS'),
            [
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,  // erro vira exceção
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,        // fetch devolve array assoc.
                PDO::ATTR_EMULATE_PREPARES   => false,                   // prepared statement REAL
            ]
        );
    }
    return $pdo;
}

O static $pdo faz a função devolver sempre a mesma conexão dentro da requisição: o equivalente simples do pool pro modelo do PHP (que abre e fecha por requisição).

4.3 Configuração fora do código

Host, usuário, senha e nome do banco não podem estar chumbados no código, por dois motivos: trocar de máquina (a da prova!) sem editar código, e nunca commitar senha (módulo 02, .gitignore).

🛠️Gambiarra boa

🛠️ Gambiarra boa: commita um .env.exemplo com as chaves e valores falsos (DB_PASS=troque-aqui). Na máquina da prova, cp .env.exemplo .env, preenche e pronto. Você ganha o setup documentado de graça e a banca vê profissionalismo no repositório.


5. CRUD com validação: o feijão com arroz bem-feito

CRUD é Create, Read, Update, Delete: as quatro operações sobre cada recurso. O descritivo pede os CRUDs completos. Fazer CRUD é fácil; fazer CRUD validado é o que pontua.

Validar é não confiar no que chega. Antes de gravar: os obrigatórios vieram? Os formatos batem? As regras de negócio passam?

🎯Foco de prova

🎯 A validação é onde mora o nível 3. Um CRUD que grava qualquer coisa é nível 2. Um CRUD que recusa dado inválido com resposta clara (400 + a lista do que está errado) é nível 3. E lembra: a validação de verdade é no back. O front valida pra dar experiência boa (módulo 05); o back valida pra garantir.

5.1 O validador reutilizável (escreve uma vez, usa em todo endpoint)

Validar na mão em cada endpoint gera código repetido e esquecimento. O padrão de mercado é um validador genérico que recebe os dados e as regras:

Node:

// src/helpers/validar.js
function validar(dados, regras) {
  const erros = [];
  for (const [campo, r] of Object.entries(regras)) {
    const valor = dados?.[campo];
    const vazio = valor === undefined || valor === null || valor === '';

    if (r.obrigatorio && vazio) {
      erros.push(`${campo} é obrigatório`);
      continue;
    }
    if (vazio) continue;  // opcional não preenchido: ok

    if (r.max && String(valor).length > r.max)
      erros.push(`${campo} passa de ${r.max} caracteres`);
    if (r.regex && !r.regex.test(String(valor)))
      erros.push(`${campo} está num formato inválido`);
    if (r.inteiro && !Number.isInteger(Number(valor)))
      erros.push(`${campo} precisa ser um número inteiro`);
  }
  return erros;
}

module.exports = validar;

PHP:

<?php
// src/helpers/validar.php
function validar(array $dados, array $regras): array {
    $erros = [];
    foreach ($regras as $campo => $r) {
        $valor = $dados[$campo] ?? null;
        $vazio = $valor === null || $valor === '';

        if (($r['obrigatorio'] ?? false) && $vazio) {
            $erros[] = "$campo é obrigatório";
            continue;
        }
        if ($vazio) continue;

        if (isset($r['max']) && mb_strlen((string)$valor) > $r['max'])
            $erros[] = "$campo passa de {$r['max']} caracteres";
        if (isset($r['regex']) && !preg_match($r['regex'], (string)$valor))
            $erros[] = "$campo está num formato inválido";
        if (($r['inteiro'] ?? false) && filter_var($valor, FILTER_VALIDATE_INT) === false)
            $erros[] = "$campo precisa ser um número inteiro";
    }
    return $erros;
}

São 25 linhas que cobrem 90% da validação da prova inteira. O que muda por endpoint é só o conjunto de regras.

5.2 O POST /convidados completo em Node

As três camadas em ação. Controller:

// src/controllers/convidados.controller.js
const validar = require('../helpers/validar');
const service = require('../services/convidados.service');

const regrasConvidado = {
  nome:          { obrigatorio: true, max: 100 },
  email:         { regex: /^\S+@\S+\.\S+$/, max: 150 },
  telefone:      { max: 15 },
  acompanhantes: { inteiro: true },
  casamento_id:  { obrigatorio: true, inteiro: true },
};

async function criar(req, res, next) {
  try {
    const erros = validar(req.body, regrasConvidado);
    if (erros.length > 0) {
      return res.status(400).json({ erro: 'Dados inválidos', detalhes: erros });
    }
    const convidado = await service.criar(req.body);
    res.status(201).json(convidado);
  } catch (erro) {
    next(erro);  // manda pro tratador central (seção 12)
  }
}

Serviço (a regra de negócio que o controller não conhece):

// src/services/convidados.service.js
const repo = require('../repositories/convidados.repository');

async function criar(dados) {
  if (dados.mesa_id) {
    const livres = await repo.lugaresLivresNaMesa(dados.mesa_id);
    if (livres < 1 + (dados.acompanhantes ?? 0)) {
      const erro = new Error('A mesa não tem lugares suficientes');
      erro.status = 409;
      throw erro;
    }
  }
  return repo.inserir(dados);
}

Repositório (só SQL):

// src/repositories/convidados.repository.js
const pool = require('../config/db');

async function inserir(c) {
  const [resultado] = await pool.query(
    `INSERT INTO convidado (casamento_id, mesa_id, nome, email, telefone, acompanhantes)
     VALUES (?, ?, ?, ?, ?, ?)`,
    [c.casamento_id, c.mesa_id ?? null, c.nome, c.email ?? null,
     c.telefone ?? null, c.acompanhantes ?? 0]
  );
  return { id: resultado.insertId, ...c };
}

5.3 O mesmo endpoint em PHP

No PHP sem framework as camadas podem morar em funções, mas a separação é a mesma:

<?php
// src/controllers/convidados.controller.php
function criarConvidado(array $corpo): void {
    $erros = validar($corpo, [
        'nome'          => ['obrigatorio' => true, 'max' => 100],
        'email'         => ['regex' => '/^\S+@\S+\.\S+$/', 'max' => 150],
        'telefone'      => ['max' => 15],
        'acompanhantes' => ['inteiro' => true],
        'casamento_id'  => ['obrigatorio' => true, 'inteiro' => true],
    ]);
    if ($erros) erro(400, 'Dados inválidos', $erros);

    // regra de negócio (num projeto maior, iria pro service)
    if (!empty($corpo['mesa_id'])) {
        $livres = lugaresLivresNaMesa((int)$corpo['mesa_id']);
        if ($livres < 1 + (int)($corpo['acompanhantes'] ?? 0)) {
            erro(409, 'A mesa não tem lugares suficientes');
        }
    }

    $pdo = conectar();
    $stmt = $pdo->prepare(
        'INSERT INTO convidado (casamento_id, mesa_id, nome, email, telefone, acompanhantes)
         VALUES (?, ?, ?, ?, ?, ?)'
    );
    $stmt->execute([
        $corpo['casamento_id'],
        $corpo['mesa_id'] ?? null,
        $corpo['nome'],
        $corpo['email'] ?? null,
        $corpo['telefone'] ?? null,
        $corpo['acompanhantes'] ?? 0,
    ]);

    json(201, ['id' => (int)$pdo->lastInsertId()] + $corpo);
}

O resto do CRUD segue o mesmo molde. Dois detalhes que valem nota:

⚠️Pega-ratão

⚠️ Pega-ratão: validar só no front. É o mesmo erro da autorização: bonito pro usuário, furado pra quem chama a API direto. Validação no front melhora a experiência; validação no back protege os dados. A banca chama a API direto.


6. SQL injection: o ataque que a banca conhece pelo nome

Segurança web é conteúdo explícito da UC13, e injection é o item número 1 de toda lista de riscos (OWASP) há vinte anos. Entender o ataque é o que faz a defesa deixar de ser decoreba.

6.1 O ataque

Imagina um login montado por concatenação:

// ⚠️ NUNCA FAZER: a query montada colando strings
const sql = `SELECT * FROM usuario
             WHERE email = '${email}' AND senha = '${senha}'`;

Agora alguém digita no campo senha: ' OR '1'='1. A query final vira:

SELECT * FROM usuario
WHERE email = 'qualquer@coisa.com' AND senha = '' OR '1'='1';

'1'='1' é sempre verdadeiro, o OR engole o resto, a consulta devolve todos os usuários e o atacante entra sem senha. O dado do usuário virou código SQL. Esse é o problema inteiro em uma frase.

6.2 A defesa: prepared statements (que você já está usando)

Prepared statement separa a viagem: a query vai numa via, os dados vão na outra, e o MySQL nunca interpreta dado como SQL. O ' OR '1'='1 vira literalmente uma senha esquisita sendo procurada, e não acha nada.

Node (mysql2), o ? é o placeholder:

const [linhas] = await pool.query(
  'SELECT * FROM usuario WHERE email = ?',
  [email]
);

PHP (PDO), prepare + execute:

$stmt = $pdo->prepare('SELECT * FROM usuario WHERE email = ?');
$stmt->execute([$email]);
$usuario = $stmt->fetch();

A regra é binária e sem exceção: variável na query = placeholder. Todos os exemplos deste módulo já seguem isso; a seção existe pra você saber explicar o porquê (pergunta clássica de banca e material de apresentação).

6.3 Onde o placeholder não chega: ORDER BY e a whitelist

Placeholder funciona pra valores, não pra nomes de coluna ou direção de ordenação. ORDER BY ? não faz o que parece. Quando a ordenação vem do usuário (seção 13), a defesa é outra: whitelist.

const colunasPermitidas = ['nome', 'criado_em', 'acompanhantes'];
const ordem   = colunasPermitidas.includes(req.query.ordenar) ? req.query.ordenar : 'nome';
const direcao = req.query.direcao === 'desc' ? 'DESC' : 'ASC';

const sql = `SELECT * FROM convidado ORDER BY ${ordem} ${direcao}`;

A interpolação aqui é segura porque ordem e direcao só podem ser valores que você mesma escreveu. O usuário escolhe da sua lista, nunca injeta texto livre. Mesmo raciocínio em PHP com in_array().

🎯Foco de prova

🎯 Foco de prova: "por que você usou prepared statement?" é pergunta de banca com resposta pronta: "porque é a única defesa correta contra SQL injection: o dado viaja separado da query e nunca é interpretado como SQL". Trinta segundos, nível 3 de maturidade.


7. Senha no back: bcrypt e password_hash

O módulo 03 (seção 18.3) fechou o lado do banco: a coluna é senha_hash VARCHAR(255) e senha em texto puro nunca é gravada. Agora o lado da API: quem gera e quem confere o hash.

Node (bcryptjs):

const bcrypt = require('bcryptjs');

// no cadastro de usuário:
const hash = await bcrypt.hash(senha, 10);   // 10 = custo (rounds)

// no login:
const bate = await bcrypt.compare(senhaDigitada, usuario.senha_hash);

PHP (nativo, sem instalar nada):

// no cadastro:
$hash = password_hash($senha, PASSWORD_DEFAULT);   // bcrypt por padrão

// no login:
$bate = password_verify($senhaDigitada, $usuario['senha_hash']);

Por que existe compare/verify em vez de comparar strings: o bcrypt embute um sal aleatório em cada hash, então a mesma senha gera hashes diferentes a cada cadastro. Só a função sabe extrair o sal e refazer a conta. Consequências práticas:

🎯Foco de prova

🎯 Foco de prova (objetivo): senha com hash é item de checklist da banca. Abrir a tabela usuario e ver $2b$10$... em vez de 123456 é a diferença entre pontuar e zerar o critério. E saber explicar o sal é o upgrade de julgamento por cima do ponto objetivo.


8. Autenticação com JWT: o crachá do sistema

🎯Foco de prova

🎯 Foco de prova (objetivo): o descritivo pede autenticação com JWT. Ou está lá funcionando, ou é zero.

Autenticação responde quem é você (o login). Autorização (seção 9) responde o que você pode fazer. O JWT é a ponte entre as duas: o crachá que o login emite e que toda rota protegida confere.

8.1 A anatomia do token

Um JWT são três blocos de texto separados por ponto: header.payload.signature.

{
  "id": 3,
  "nome": "Ana",
  "perfil": "recepcao",
  "exp": 1767225600
}
⚠️Pega-ratão

⚠️ Pega-ratão que derruba gente experiente: o payload é apenas codificado em Base64, não criptografado. Qualquer um cola o token no jwt.io e lê o conteúdo. O que o token garante é que ninguém alterou (integridade), não que ninguém leu (sigilo). Logo: id, nome e perfil no payload, sim; senha ou qualquer dado sensível, jamais.

O campo exp é a expiração em timestamp. Token vencido = 401, mesmo com assinatura válida.

8.2 O login que emite o token

Node (jsonwebtoken):

// src/controllers/auth.controller.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const pool = require('../config/db');

async function login(req, res, next) {
  try {
    const { email, senha } = req.body;
    const [linhas] = await pool.query('SELECT * FROM usuario WHERE email = ?', [email]);
    const usuario = linhas[0];

    if (!usuario || !(await bcrypt.compare(senha, usuario.senha_hash))) {
      return res.status(401).json({ erro: 'E-mail ou senha inválidos' });
    }

    const token = jwt.sign(
      { id: usuario.id, nome: usuario.nome, perfil: usuario.perfil },
      process.env.JWT_SECRET,
      { expiresIn: '8h' }
    );

    res.json({
      token,
      usuario: { id: usuario.id, nome: usuario.nome, perfil: usuario.perfil },
    });
  } catch (erro) { next(erro); }
}

PHP (firebase/php-jwt, via composer require firebase/php-jwt):

<?php
use Firebase\JWT\JWT;

function login(array $corpo): void {
    $pdo = conectar();
    $stmt = $pdo->prepare('SELECT * FROM usuario WHERE email = ?');
    $stmt->execute([$corpo['email'] ?? '']);
    $usuario = $stmt->fetch();

    if (!$usuario || !password_verify($corpo['senha'] ?? '', $usuario['senha_hash'])) {
        erro(401, 'E-mail ou senha inválidos');
    }

    $token = JWT::encode([
        'id'     => $usuario['id'],
        'nome'   => $usuario['nome'],
        'perfil' => $usuario['perfil'],
        'exp'    => time() + 60 * 60 * 8,   // 8 horas
    ], getenv('JWT_SECRET'), 'HS256');

    json(200, [
        'token'   => $token,
        'usuario' => ['id' => $usuario['id'], 'nome' => $usuario['nome'], 'perfil' => $usuario['perfil']],
    ]);
}

Dois detalhes de segurança embutidos aí:

8.3 O middleware que protege as rotas

Node:

// src/middlewares/auth.js
const jwt = require('jsonwebtoken');

function exigeLogin(req, res, next) {
  const cabecalho = req.headers.authorization ?? '';
  if (!cabecalho.startsWith('Bearer ')) {
    return res.status(401).json({ erro: 'Token não enviado' });
  }
  try {
    req.usuario = jwt.verify(cabecalho.slice(7), process.env.JWT_SECRET);
    next();
  } catch {
    return res.status(401).json({ erro: 'Token inválido ou expirado' });
  }
}

O jwt.verify confere assinatura e expiração de uma vez. Passou: os dados do crachá ficam em req.usuario pra qualquer camada usar (quem registrou o check-in? req.usuario.id). A rota usa o middleware como visto na seção 3.2.

PHP (sem middleware nativo, a proteção é a primeira linha do controller):

<?php
// src/helpers/auth.php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

function exigeLogin(): object {
    $cabecalho = $_SERVER['HTTP_AUTHORIZATION']
        ?? (function_exists('getallheaders') ? (getallheaders()['Authorization'] ?? '') : '');

    if (!str_starts_with($cabecalho, 'Bearer ')) {
        erro(401, 'Token não enviado');
    }
    try {
        return JWT::decode(substr($cabecalho, 7), new Key(getenv('JWT_SECRET'), 'HS256'));
    } catch (Exception $e) {
        erro(401, 'Token inválido ou expirado');
    }
}

E dentro de qualquer controller protegido:

function removerConvidado(?string $id): void {
    $usuario = exigeLogin();                        // barra com 401 se não passar
    exigePerfil($usuario, 'admin');                 // seção 9: barra com 403
    // ... daqui pra baixo, só quem pode
}
⚠️Pega-ratão

⚠️ Pega-ratão específico do PHP: o Apache às vezes descarta o header Authorization antes de chegar no PHP (aí $_SERVER['HTTP_AUTHORIZATION'] vem vazio e todo mundo toma 401 "sem motivo"). O fallback com getallheaders() acima resolve; com o servidor embutido (php -S) o problema nem existe. Se um dia a auth "parar do nada" no Apache, o suspeito é esse.

8.4 Expiração e onde o front guarda


9. Autorização por perfil (roles): o que cada crachá abre

O token carrega o perfil (o ENUM do módulo 03: admin, organizador, recepcao). Cada rota declara quais perfis aceita, e a API barra o resto com 403.

Node (o segundo middleware, parametrizado):

// src/middlewares/auth.js (continuação)
function exigePerfil(...perfis) {
  return (req, res, next) => {
    if (!perfis.includes(req.usuario.perfil)) {
      return res.status(403).json({ erro: 'Seu perfil não tem permissão pra essa ação' });
    }
    next();
  };
}

module.exports = { exigeLogin, exigePerfil };

Na rota, os dois em sequência (a ordem importa: primeiro sabe quem é, depois confere o que pode):

router.delete('/:id', exigeLogin, exigePerfil('admin'), controller.remover);
router.post('/:id/checkin', exigeLogin, exigePerfil('recepcao', 'admin'), controller.checkin);

PHP:

<?php
// src/helpers/auth.php (continuação)
function exigePerfil(object $usuario, string ...$perfis): void {
    if (!in_array($usuario->perfil, $perfis, true)) {
        erro(403, 'Seu perfil não tem permissão pra essa ação');
    }
}

A tabela de quem pode o quê já está pronta: é a coluna "Quem pode" do mapa da seção 2.4. Implementar autorização é transcrever aquela coluna pra middlewares.

🎯Foco de prova

🎯 Foco de prova: o sistema tem três perfis com poderes diferentes, e a autorização precisa estar no back. Testar isso é fácil pra banca: loga como recepção, copia o token, chama DELETE /convidados/12 no Postman. Se apagar, o controle de acesso inteiro vale zero, não importa quão bonito o front esconde o botão. O roteiro de teste da seção 15 inclui exatamente essa chamada.

⚠️ Pega-ratão: proteger as rotas "importantes" e esquecer as secundárias. GET /convidados aberto sem token vaza a lista inteira de convidados pra qualquer um. A regra: toda rota é protegida por padrão; pública é exceção declarada de propósito (login, página de RSVP).


10. CORS: deixar o front conversar com o back

Quando o front (por exemplo http://localhost:5500) chama a API (http://localhost:3000), o navegador bloqueia por segurança, a não ser que a API diga "pode, eu confio nessa origem". Isso é o CORS (Cross-Origin Resource Sharing). Detalhe que confunde: quem barra é o navegador do lado do front, mas quem autoriza é o servidor. Por isso a correção é sempre no back.

Tem ainda o preflight: antes de um POST com JSON ou com header Authorization, o navegador manda sozinho uma requisição OPTIONS perguntando "posso?". Se a API não responder o OPTIONS direito, o POST nem acontece, e o sintoma é o famoso "front não conecta".

Node: o pacote cors resolve tudo, inclusive o preflight:

const cors = require('cors');
app.use(cors());   // liberado geral: perfeito pra desenvolvimento e prova

PHP: os headers no topo do index.php, mais o tratamento do OPTIONS:

header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');

if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    http_response_code(204);
    exit;   // preflight respondido, o navegador libera a requisição real
}
🎯Foco de prova

🎯 Foco de prova: o descritivo cita liberar CORS. É objetivo e é a causa número 1 de "meu front não conecta na API" no dia da prova. O erro aparece no console do navegador (em inglês, módulo 09: "blocked by CORS policy"), nunca no terminal da API, o que engana quem procura no lugar errado.

🛠️ Gambiarra boa: libera o CORS no minuto 1 do Módulo B, antes mesmo de existir front. São 2 linhas. Aí quando o front chegar no Módulo C, a ponte já está pronta e você não descobre o problema no pior momento. Em produção se restringiria a origem (origin: 'https://seusite.com'); na prova, * e bola pra frente, sabendo explicar por quê.


11. Regras de negócio no servidor: capacidade, corrida e transação

As regras que separam a API nota 2 da nota 3. Todas moram no serviço, todas têm o banco como última linha de defesa.

11.1 A regra de capacidade e o alerta de 90%

O casamento tem capacidade_total (tabela do módulo 03). A API precisa: impedir confirmação que estoure o limite e informar a ocupação pro dashboard acender o alerta de 90%.

A consulta que resolve os dois (uma subquery, módulo 03 seção 13):

SELECT
  ca.capacidade_total,
  (SELECT COALESCE(SUM(1 + cv.acompanhantes), 0)
     FROM convidado cv
     JOIN convite co ON co.convidado_id = cv.id
    WHERE cv.casamento_id = ca.id
      AND co.status = 'confirmado') AS confirmados
FROM casamento ca
WHERE ca.id = ?;

O serviço transforma em decisão:

async function statusCapacidade(casamentoId) {
  const { capacidade_total, confirmados } = await repo.capacidade(casamentoId);
  return {
    capacidade_total,
    confirmados,
    alerta90: confirmados >= capacidade_total * 0.9,
  };
}

async function confirmarPresenca(codigo) {
  const convite = await repo.buscarConvitePorCodigo(codigo);
  if (!convite) { const e = new Error('Convite não encontrado'); e.status = 404; throw e; }

  const { capacidade_total, confirmados } = await repo.capacidade(convite.casamento_id);
  const grupo = 1 + convite.acompanhantes;
  if (confirmados + grupo > capacidade_total) {
    const e = new Error('O casamento atingiu a capacidade máxima');
    e.status = 409; throw e;
  }
  return repo.confirmar(convite.id);
}

Repara: a conta inclui os acompanhantes (1 + acompanhantes por convidado). Esquecer os acompanhantes na soma é o clássico bug de regra de negócio que passa no teste rápido e explode na demo. E a conta mora no servidor porque é regra de negócio: dashboard, página de RSVP e Postman recebem o mesmo veredito.

11.2 O check-in duplo: condição de corrida e as três camadas de defesa

O descritivo pede bloquear dupla entrada: um convidado não entra duas vezes. A implementação ingênua:

1. SELECT: esse convidado já tem check-in?
2. Se não tem, INSERT.

Parece certa e tem um buraco: se dois pedidos chegam quase juntos (duas recepcionistas escaneiam o mesmo código, ou um clique duplo), os dois rodam o SELECT antes de qualquer INSERT, os dois leem "não tem", e os dois inserem. Isso é uma condição de corrida: o resultado depende da ordem de chegada em milissegundos que você não controla.

A defesa em três camadas, da cosmética à garantia:

  1. Front (módulo 05): desabilita o botão após o clique. Melhora a experiência, não garante nada (duas abas, dois computadores).
  2. API: não faz SELECT + INSERT; faz só o INSERT e trata o erro de duplicidade. Uma operação só, sem janela entre checar e agir.
  3. Banco: a constraint UNIQUE (convidado_id) na tabela checkin (módulo 03, seção 8). Essa é a garantia real. O MySQL é incapaz de aceitar o segundo INSERT, não importa quantos cheguem juntos.

O código da camada 2, que converte a recusa do banco em resposta educada:

Node:

// src/services/checkin.service.js
async function fazerCheckin(convidadoId, usuarioId) {
  try {
    const [r] = await pool.query(
      'INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)',
      [convidadoId, usuarioId]
    );
    return { id: r.insertId };
  } catch (erro) {
    if (erro.code === 'ER_DUP_ENTRY') {          // a UNIQUE barrou
      const jaEntrou = new Error('Esse convidado já fez check-in');
      jaEntrou.status = 409;
      throw jaEntrou;
    }
    throw erro;   // outro erro: deixa o tratador central cuidar
  }
}

PHP:

function fazerCheckin(?string $convidadoId): void {
    $usuario = exigeLogin();
    exigePerfil($usuario, 'recepcao', 'admin');

    try {
        $pdo = conectar();
        $stmt = $pdo->prepare('INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)');
        $stmt->execute([$convidadoId, $usuario->id]);
        json(201, ['mensagem' => 'Check-in registrado']);
    } catch (PDOException $e) {
        if (($e->errorInfo[1] ?? 0) === 1062) {   // 1062 = violação de UNIQUE no MySQL
            erro(409, 'Esse convidado já fez check-in');
        }
        throw $e;
    }
}

Variante útil quando não existe tabela própria (um campo de status na própria linha): o UPDATE atômico, que checa e age na mesma instrução:

UPDATE convite SET status = 'confirmado', respondido_em = NOW()
WHERE codigo = ? AND status = 'pendente';

Se affectedRows for 0, alguém confirmou antes (ou o código não existe): responde 409/404. Checagem e escrita numa operação só = sem janela de corrida.

🎯Foco de prova

🎯 Isso é assunto de nível 3 e de apresentação. A história pronta pro pitch do Módulo D: "o check-in tem três camadas de defesa: o botão desabilita no front, a API insere e trata a duplicidade, e a constraint UNIQUE no banco garante mesmo se dois pedidos chegarem juntos, porque existe condição de corrida entre checar e gravar". Quarenta segundos que mostram profundidade que a maioria não tem. Guarda essa pra contar no pitch.

11.3 Transações no código: tudo ou nada

O conceito veio do módulo 03 (seção 17); aqui é onde ele entra no código da API. Use quando duas ou mais escritas precisam acontecer juntas: confirmar o convite E atribuir a mesa; remanejar um convidado E liberar o lugar antigo.

Node: transação exige a mesma conexão do começo ao fim, então se pede uma emprestada ao pool:

const conexao = await pool.getConnection();
try {
  await conexao.beginTransaction();

  await conexao.query(
    "UPDATE convite SET status = 'confirmado', respondido_em = NOW() WHERE id = ?",
    [conviteId]
  );
  await conexao.query(
    'UPDATE convidado SET mesa_id = ? WHERE id = ?',
    [mesaId, convidadoId]
  );

  await conexao.commit();      // as duas valem
} catch (erro) {
  await conexao.rollback();    // nenhuma vale
  throw erro;
} finally {
  conexao.release();           // devolve a conexão pro pool, SEMPRE
}

PHP (PDO):

$pdo = conectar();
try {
    $pdo->beginTransaction();

    $pdo->prepare("UPDATE convite SET status = 'confirmado', respondido_em = NOW() WHERE id = ?")
        ->execute([$conviteId]);
    $pdo->prepare('UPDATE convidado SET mesa_id = ? WHERE id = ?')
        ->execute([$mesaId, $convidadoId]);

    $pdo->commit();
} catch (Exception $e) {
    $pdo->rollBack();
    throw $e;
}
⚠️Pega-ratão

⚠️ Pega-ratão do Node: rodar beginTransaction numa conexão e as queries em pool.query(). O pool pode entregar outra conexão pra cada query, e aí metade da "transação" roda fora dela. Transação no mysql2 = getConnection() + tudo pela mesma conexao + release() no finally. Esquecer o release vaza conexões até o pool secar e a API travar (10 requisições depois, do nada).


12. Tratamento de erro padronizado: um formato só pra tudo

Toda resposta de erro da API, de 400 a 500, no mesmo formato:

{ "erro": "mensagem legível pra quem usa", "detalhes": ["campo nome é obrigatório"] }

Por quê: o front (módulo 05) escreve um tratador pra exibir qualquer erro, a banca vê consistência, e você nunca perde tempo decidindo formato de novo. Os exemplos deste módulo já seguem esse formato; aqui é a central que garante ele até pros erros que você não previu.

Node: o middleware de erro (4 parâmetros, registrado por último no server.js):

// src/middlewares/erros.js
module.exports = (erro, req, res, next) => {
  const status = erro.status ?? 500;
  const mensagem = status === 500 ? 'Erro interno no servidor' : erro.message;

  if (status === 500) console.error(erro);   // stack trace no terminal, pra VOCÊ

  res.status(status).json({ erro: mensagem, detalhes: erro.detalhes ?? [] });
};

O padrão do time inteiro: serviço lança Error com .status; controller faz try/catch e chama next(erro); a central formata. Erro esperado (400, 404, 409) sai com a mensagem que o serviço escreveu; erro inesperado sai como 500 genérico.

PHP: o try/catch (Throwable) global do index.php (seção 3.3) já é a central. Erros esperados saem pelo helper erro() no meio do caminho; qualquer exceção não tratada vira error_log($e) + 500 genérico.

Por que o 500 é genérico de propósito: a mensagem crua de uma exceção pode vazar caminho de arquivo, SQL e estrutura interna. Isso é falha de segurança (informação pra atacante) e de acabamento (usuário vendo stack trace). O detalhe completo vai pro log/terminal, que é onde você debuga; pro cliente vai "Erro interno no servidor".

🎯Foco de prova

🎯 Foco de prova: mensagens de erro claras e consistentes são critério de julgamento, e o módulo 08 (lapidação) chama isso de "o ganho de nível 3 mais barato". A central de erro é o alicerce disso no back: 15 linhas, escritas uma vez, elevam TODAS as respostas de erro do sistema.


13. Paginação, filtro e ordenação no servidor

A UC13 cita ordenação e filtragem explicitamente, e o Wedding Pass tem a tela perfeita pra isso: a lista de convidados (com busca, filtro por mesa, ordenação e páginas). O contrato:

GET /convidados?busca=silva&mesa_id=3&ordenar=nome&direcao=asc&pagina=2&por_pagina=20

Tudo opcional, tudo com default sensato. A implementação (Node; em PHP muda $_GET e a sintaxe, a lógica é idêntica):

// src/repositories/convidados.repository.js
async function listar(filtros) {
  const clausulas = [];
  const valores = [];

  if (filtros.busca) {
    clausulas.push('nome LIKE ?');
    valores.push(`%${filtros.busca}%`);
  }
  if (filtros.mesa_id) {
    clausulas.push('mesa_id = ?');
    valores.push(Number(filtros.mesa_id));
  }

  const where = clausulas.length ? `WHERE ${clausulas.join(' AND ')}` : '';

  // whitelist (seção 6.3): nome de coluna NUNCA vem direto do usuário
  const ordem   = ['nome', 'criado_em', 'acompanhantes'].includes(filtros.ordenar)
                  ? filtros.ordenar : 'nome';
  const direcao = filtros.direcao === 'desc' ? 'DESC' : 'ASC';

  const porPagina = Math.min(Number(filtros.por_pagina) || 20, 100);  // teto anti-abuso
  const offset    = ((Number(filtros.pagina) || 1) - 1) * porPagina;

  const [linhas] = await pool.query(
    `SELECT * FROM convidado ${where} ORDER BY ${ordem} ${direcao} LIMIT ? OFFSET ?`,
    [...valores, porPagina, offset]
  );

  const [[{ total }]] = await pool.query(
    `SELECT COUNT(*) AS total FROM convidado ${where}`, valores
  );

  return { dados: linhas, total, pagina: Number(filtros.pagina) || 1, por_pagina: porPagina };
}

Os pontos que valem nota nesse bloco:

🛠️ Na prova, calibra o esforço: se a lista é pequena (mesas de um casamento), filtrar no front resolve e é honesto. Mas a lista de convidados é a lista grande do domínio, e o descritivo pede filtragem no servidor: implementa nela o pacote completo e aponta isso na apresentação. Um endpoint exemplar vale mais que cinco medianos.


14. ORM: o que é, e por que o SQL direto ganha a prova

A UC9 pede o conceito de ORM, então ele pode virar pergunta de banca mesmo que você nunca use um na prova.

ORM (Object-Relational Mapping) é a biblioteca que mapeia tabela em objeto e esconde o SQL: você escreve Convidado.findAll({ where: { mesa_id: 3 } }) e o ORM gera o SELECT. No mercado: Sequelize e Prisma no Node; Eloquent (Laravel) e Doctrine no PHP.

O que o ORM compra, e o que cobra:

ORM SQL direto (nosso caminho)
Produtividade em projeto grande alta (migrations, relações prontas) manual
Setup inicial modelos, config, docs da lib zero: a conexão da seção 4
Controle sobre a query você confia no que ele gera total: a query é a que você escreveu
Debug camada extra entre você e o erro o erro do MySQL na cara
Na prova de 2h30 tempo de setup que você não tem começa a pontuar no minuto 5

A decisão pra Seletiva é clara: SQL direto com prepared statements. Os motivos, prontos pra apresentação: o tempo de setup do ORM não se paga em 2h30; a banca avalia o SQL (módulo 03 vale 10% sozinho) e o SQL escondido no ORM não mostra domínio; e o debug transparente vale ouro sob pressão. A resposta nota 3 pra "por que não usou ORM?": "conheço o conceito, mapeia tabelas em objetos e agiliza projetos grandes, mas numa prova curta o SQL direto me dá controle, velocidade e mostra o domínio que está sendo avaliado".


15. Testar a API sem front: Postman, Insomnia e debug

O Módulo B termina antes de existir front. Como provar que a API funciona? Postman ou Insomnia (os dois servem; escolhe um e domina). O cronograma reserva tempo pra isso na semana da API, e a banca pode pedir pra ver.

Organização que rende: uma coleção por sistema, uma pasta por recurso (auth, convidados, mesas, check-in), uma requisição salva por endpoint. Duas variáveis de ambiente: {{base_url}} (troca localhost pela URL da prova em um lugar só) e {{token}} (cola o token do login uma vez, toda requisição usa Bearer {{token}} no header Authorization).

O roteiro de teste de cada endpoint protegido (4 chamadas, sempre as mesmas):

  1. Sem token → tem que dar 401.
  2. Com token de perfil errado (recepção tentando DELETE) → tem que dar 403.
  3. Com dado inválido (nome vazio) → tem que dar 400 com a lista de detalhes.
  4. Com tudo certo → 200/201 e o dado confere no banco.

Mais os especiais: check-in duas vezes no mesmo convidado (a segunda tem que dar 409) e GET de id inexistente (404). Se a coleção inteira passa nesse roteiro, a API está defendida, e essa frase é literalmente o fechamento do Módulo B.

Quando dá errado, o kit de debug:


16. Checklist de segurança do Módulo B

Os riscos do OWASP Top 10 filtrados pro que a banca enxerga e testa, cada um com defesa já construída neste módulo:

Risco (OWASP) Como apareceria na prova Defesa Seção
Injection login furado com ' OR '1'='1 prepared statements sempre; whitelist no ORDER BY 6
Falha de autenticação senha em texto puro; token sem expiração bcrypt/password_hash; JWT com exp e segredo no .env 7, 8
Falha de controle de acesso endpoint aberto; perfil não conferido exigeLogin + exigePerfil em toda rota não pública 9
Exposição de dados stack trace na resposta; senha_hash no JSON 500 genérico; nunca devolver a coluna de senha 12

O último item merece o reforço: SELECT * na tabela usuario devolve o senha_hash junto, e ele vaza pro front no JSON do login ou da listagem. Lista as colunas (SELECT id, nome, email, perfil FROM usuario) ou remove o campo antes do res.json. É o tipo de vazamento que a banca acha em 10 segundos abrindo a aba Network.

A checklist de fim de Módulo B, pra rodar nos últimos 10 minutos:


17. Autoavaliação do módulo

  1. O que quer dizer a API ser "agnóstica ao cliente", e por que a regra de negócio mora nela?
  2. Qual a diferença entre conteúdo estático e dinâmico, e onde cada um vive no Wedding Pass?
  3. Qual verbo HTTP pra cada ação (criar, buscar, atualizar, apagar)? O que é idempotência?
  4. Qual a diferença entre 401 e 403? E quando usar 409?
  5. O que faz cada camada (controller, serviço, repositório) e qual a regra de ouro entre elas?
  6. Por que usar pool de conexões no Node? E o que fazem as três opções do PDO na conexão?
  7. Como funciona o ataque ' OR '1'='1 e por que o prepared statement barra ele?
  8. Por que ORDER BY ? não funciona e qual a defesa certa pra ordenação vinda do usuário?
  9. Por que existe bcrypt.compare/password_verify em vez de comparar os hashes com ===?
  10. Quais as três partes de um JWT e o que cada uma garante? O que NUNCA vai no payload, e por quê?
  11. Qual a diferença entre autenticação e autorização, e onde cada middleware entra?
  12. O que é o preflight (OPTIONS) do CORS e qual o sintoma clássico quando ele não é tratado?
  13. O que é a condição de corrida no check-in e quais as três camadas de defesa?
  14. Quando usar transação na API, e por que no Node ela exige getConnection() em vez de pool.query()?
  15. Por que a resposta de erro 500 tem que ser genérica, e pra onde vai o detalhe do erro?
  16. Se a banca perguntar "por que você não usou ORM?", qual a resposta nota 3?

Referências do módulo

Bibliografia do plano de curso (UC13/UC9):

Segurança:

Referências de mercado (pesquisa web):


💡Nota

Back-end no lugar? Agora vem o módulo mais pesado da prova, onde tudo isso vira tela: 05 — Front-end web.

Competições Senac RS · Seletiva 26

Módulo 05

Módulo 05 — Front-end web

BaseUC12 (Desenvolver front-end web, 96h)Peso na prova40% (o maior de todos)NaturezaRevisão
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Por que esse é o módulo mais gordo. O front vale 40% da nota, mais que back e banco somados. É o Módulo C da prova (4 horas, o mais longo). É a parte que a banca vê funcionando, e "o que a banca vê" é metade do jogo (lembra do módulo 01). Aqui é onde 2 vira 3 ou vira 1. Natureza 🔄 revisão, as duas já fizeram front na Regional, mas é aqui que a gente coloca mais energia de treino, porque é onde tem mais nota pra ganhar e mais pra perder. O combinado do módulo: uma stack só no front (HTML + CSS + JavaScript puro, sem framework), porque é o que a prova permite com segurança e o que dá controle total sob pressão. Todo código desse módulo conversa com a API que vocês construíram no módulo 04: mesmas rotas, mesmo token, mesmo formato de erro.


1. As três camadas e a organização do projeto

Todo front é feito de três linguagens com papéis distintos. Misturar os papéis é a origem de metade da bagunça:

🛠️Gambiarra boa

🛠️ Gambiarra boa: separação de responsabilidades. Estrutura no HTML, estilo no CSS, lógica no JS. Não jogar estilo dentro do HTML (style= no meio da tag) nem lógica embolada com aparência. Código separado é código que a banca lê fácil (ponto de julgamento) e que você conserta rápido sob pressão.

1.1 A pasta do front

A separação de papéis vira separação de arquivos. O projeto do Módulo C nasce assim nos primeiros minutos:

front/
├── index.html          ← login (a porta de entrada)
├── convidados.html     ← lista + cadastro
├── checkin.html        ← a tela da recepção
├── dashboard.html      ← indicadores da gestão
├── css/
│   └── style.css       ← um CSS só, compartilhado por todas as páginas
└── js/
    ├── api.js          ← comunicação com a API (seção 5.6)
    ├── auth.js         ← token, guarda de página, perfil (seção 8)
    ├── convidados.js   ← lógica da tela de convidados
    ├── checkin.js
    └── dashboard.js

Decisões embutidas aí:

Os scripts entram no fim com defer, que garante que o HTML já existe quando o JS rodar:

<script src="js/api.js" defer></script>
<script src="js/auth.js" defer></script>
<script src="js/convidados.js" defer></script>
⚠️Pega-ratão

⚠️ Pega-ratão: script no <head> sem defer roda antes do HTML existir. Aí o querySelector devolve null e o console grita Cannot read properties of null. É um dos erros mais comuns de front e se resolve com uma palavra.


2. HTML semântico: estrutura que significa algo

HTML semântico é usar a tag certa pro sentido certo, em vez de div pra tudo. Um cabeçalho é header, uma navegação é nav, o conteúdo principal é main, um botão é button.

Por que importa:

2.1 O esqueleto de toda página

Esse bloco abre todo HTML da prova. Vale digitar de memória:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Wedding Pass · Convidados</title>
  <link rel="stylesheet" href="css/style.css">
</head>
<body>
  <header class="navbar"> ... </header>
  <main> ... </main>
  <script src="js/api.js" defer></script>
</body>
</html>
⚠️Pega-ratão

⚠️ Pega-ratão clássico da prova: tela "responsiva" que funciona no DevTools do notebook e quebra no celular de verdade. Causa número 1: esqueceu a meta viewport. É a segunda linha do <head>, sempre.

2.2 As tags que a prova usa

Não precisa decorar a especificação inteira. Essa tabela cobre o Wedding Pass:

Tag Pra quê Onde aparece na prova
header topo da página ou de uma seção barra superior com logo e navegação
nav bloco de navegação menu entre as telas
main conteúdo principal (um por página) tudo que não é topo nem rodapé
section agrupamento temático com título bloco de indicadores, bloco de busca
article item independente e completo um card de convidado
footer rodapé créditos, versão
h1...h3 hierarquia de títulos (sem pular níveis) h1 o título da tela, h2 as seções
form, label, input, select, button entrada de dados cadastro, login, busca
table, thead, tbody, th, td dados tabulares lista de convidados em tela larga
ul, li listas menu, lista de erros de validação
strong, span ênfase e gancho de estilo destaque de números, badges
⚠️Pega-ratão

⚠️ Pega-ratão: a "div soup". Página inteira feita de div genérica, botão que é um div com clique. Funciona visualmente, mas é frágil, inacessível e nível baixo de qualidade. Um div com onclick não recebe foco por Tab, não dispara com Enter e é invisível pra leitor de tela. button faz tudo isso de graça. div e span existem, mas são o último recurso (agrupar pra estilizar), não o primeiro.

2.3 O formulário bem feito

Formulário é onde marcação semântica vira nota mais rápido. O cadastro de convidado, do jeito certo:

<form id="form-convidado" novalidate>
  <div class="campo">
    <label for="nome">Nome completo</label>
    <input type="text" id="nome" name="nome" required minlength="3"
           placeholder="Maria da Silva">
    <p class="campo-erro" hidden></p>
  </div>

  <div class="campo">
    <label for="email">E-mail</label>
    <input type="email" id="email" name="email" placeholder="maria@email.com">
    <p class="campo-erro" hidden></p>
  </div>

  <div class="campo">
    <label for="telefone">Telefone</label>
    <input type="tel" id="telefone" name="telefone" placeholder="(51) 99999-9999">
    <p class="campo-erro" hidden></p>
  </div>

  <div class="campo">
    <label for="acompanhantes">Acompanhantes (máx. 5)</label>
    <input type="number" id="acompanhantes" name="acompanhantes" min="0" max="5" value="0">
    <p class="campo-erro" hidden></p>
  </div>

  <div class="acoes">
    <button type="button" class="botao-secundario" id="btn-cancelar">Cancelar</button>
    <button type="submit" class="botao-primario">Salvar convidado</button>
  </div>
</form>

O que esse bloco carrega:


3. CSS de verdade: do seletor ao layout

CSS é onde os 40% mais aparecem. A diferença entre uma tela nota 2 e nota 3 quase nunca é "sabia a propriedade"; é controle: saber por que a regra aplicou (ou não), espaçar com ritmo, alinhar sem gambiarra de margem.

3.1 Seletores, especificidade e cascata

Os seletores que resolvem a prova:

Seletor Pega o quê Exemplo de uso
.card classe o dia a dia: quase tudo
button elemento estilos de base (reset, fonte)
.card .badge descendente badge só quando está dentro de card
.card > h3 filho direto título imediato do card
input[type="email"] atributo estilizar tipos específicos
:hover, :focus, :active, :disabled pseudo-classes de estado botões e campos reagindo
:nth-child(even) posição zebrar linhas de tabela
:not(.ativo) negação todos menos o selecionado
::before, ::after pseudo-elementos ícones decorativos, setas
::placeholder o texto de exemplo do input placeholder mais suave

Quando duas regras disputam o mesmo elemento, ganha a mais específica. A ordem de força:

  1. style= inline (que a gente não usa)
  2. #id
  3. .classe, [atributo], :pseudo-classe
  4. elemento, ::pseudo-elemento

Empatou a especificidade? Ganha a que aparece por último no arquivo (a cascata).

🛠️Gambiarra boa

🛠️ Gambiarra boa: especificidade plana. Na prova, estilizar só com classes (#id fica pro JS achar elementos, não pro CSS). Com tudo no mesmo nível de força, a cascata fica previsível: a última regra ganha, ponto. Nunca usar !important: ele "resolve" agora e cobra depois, porque a próxima correção vai precisar de outro !important, e em 20 minutos o CSS vira um cabo de guerra. Se você sentiu necessidade de !important, tem uma regra mais específica escondida: acha ela no DevTools (aba Styles mostra riscado quem perdeu a disputa).

:focus não é opcional. Campo e botão precisam mostrar onde o teclado está:

button:focus-visible,
input:focus {
  outline: 2px solid var(--cor-acao);
  outline-offset: 2px;
}

Tirar o outline sem pôr nada no lugar (outline: none seco) é erro de acessibilidade que o módulo 06 pontua.

3.2 O modelo de caixa e a escala de espaçamento

Todo elemento é uma caixa: conteúdo, preenchimento interno (padding), borda e margem externa. O detalhe que muda tudo é como a largura é contada. No modo padrão (content-box), width: 300px + padding: 20px = caixa real de 340px, e seu layout estoura sem você entender por quê. A correção é a primeira regra de todo CSS:

/* reset mínimo de prova: as três linhas que abrem o style.css */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

Com border-box, width: 300px é 300px de verdade, padding e borda já contando. Fim das contas de padaria no meio da prova.

Espaçamento com ritmo. Espaço não se inventa a cada elemento; se escolhe de uma escala. A escala prática: múltiplos de 8px (com 4px pra ajustes finos): 4, 8, 16, 24, 32, 48. Todo padding, margin e gap da tela sai desse conjunto.

🎯Foco de prova

🎯 Espaçamento consistente é sinal de nível 3. Telas onde os espaços seguem um ritmo (sempre múltiplos de um valor base) parecem profissionais. Telas com espaçamento aleatório parecem amadoras, mesmo com as mesmas cores. A banca sente isso, mesmo sem saber nomear. Na seção 3.4 a escala vira variável e o ritmo vira automático.

3.3 Unidades: qual usar onde

Unidade O que é Usar em
px pixel fixo borda, sombra, detalhes finos
rem múltiplo da fonte raiz (16px por padrão) fonte, padding, margin, gap
em múltiplo da fonte do próprio elemento raramente; padding interno de botão que cresce com o texto
% proporção do pai larguras fluidas
vh / vw proporção da tela min-height: 100vh pra centralizar o login
fr fração do espaço livre (só no grid) colunas de grid

A regra de bolso: rem pra tamanhos que devem escalar junto (texto e espaçamento), px pro que é fino e fixo (borda de 1px é borda de 1px), % e fr pra layout. 1rem = 16px, 0.5rem = 8px, 1.5rem = 24px: a escala de espaçamento da seção 3.2 em rem.

Usar rem no texto respeita o usuário que aumentou a fonte do navegador. É acessibilidade de graça e é o padrão de mercado.

3.4 Custom properties: o design system em 20 linhas

Variáveis de CSS transformam decisões soltas em sistema. Declaradas no :root, valem na página inteira:

:root {
  /* cores: uma de ação, neutros, três de status */
  --cor-acao: #7c3aed;
  --cor-acao-hover: #6d28d9;
  --cor-texto: #1f2937;
  --cor-texto-suave: #6b7280;
  --cor-fundo: #f4f4f5;
  --cor-superficie: #ffffff;
  --cor-borda: #e4e4e7;
  --cor-sucesso: #16a34a;
  --cor-erro: #dc2626;
  --cor-alerta: #d97706;

  /* espaçamento: a escala da seção 3.2 */
  --espaco-1: 0.25rem;   /*  4px */
  --espaco-2: 0.5rem;    /*  8px */
  --espaco-3: 1rem;      /* 16px */
  --espaco-4: 1.5rem;    /* 24px */
  --espaco-5: 2rem;      /* 32px */
  --espaco-6: 3rem;      /* 48px */

  /* acabamento */
  --raio: 8px;
  --sombra: 0 1px 3px rgba(0, 0, 0, 0.12);
}

.botao-primario {
  background: var(--cor-acao);
  padding: var(--espaco-2) var(--espaco-4);
  border-radius: var(--raio);
}

O ganho é triplo:

  1. Consistência automática: todo botão, card e título bebe da mesma fonte. Não existe "esse verde é um pouco diferente daquele".
  2. Mudança global em um lugar: a banca achou o roxo escuro demais? Troca uma linha e a tela inteira acompanha. Sem variável, é caça ao #7c3aed em 400 linhas.
  3. Ponte com o módulo 06: os tokens de cor e espaço definidos no wireframe (design system mínimo) viram literalmente esse bloco :root. Papel vira código em 5 minutos.
🛠️Gambiarra boa

🛠️ Gambiarra boa: o :root é a primeira coisa que se escreve no Módulo C. Antes de qualquer tela: reset (3.2) + :root com tokens + estilos de base (fonte, botões, campos). Uns 15 minutos que compram consistência pelas 4 horas inteiras. E quando bater vontade de inventar uma cor nova no meio da prova, a pergunta é: "isso merece virar token?". Quase sempre a resposta é usar um que já existe.

3.5 Flexbox: alinhar num eixo

Flexbox distribui itens numa direção (linha ou coluna). É a ferramenta de todos os alinhamentos pequenos da tela:

.navbar {
  display: flex;
  justify-content: space-between;  /* logo pra um lado, menu pro outro */
  align-items: center;             /* tudo alinhado na vertical */
  padding: var(--espaco-3) var(--espaco-4);
}

As cinco propriedades que resolvem 95% dos casos:

No filho, uma vale ouro: flex: 1 (cresce e ocupa o espaço livre). Campo de busca que estica com botão fixo do lado:

.barra-busca { display: flex; gap: var(--espaco-2); }
.barra-busca input { flex: 1; }

Os flex do Wedding Pass: a navbar, a linha de botões do formulário (justify-content: flex-end), o topo do card (nome à esquerda, badge à direita), e o login centralizado na tela:

.tela-login {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: gap em vez de margem nos filhos. Margem em item de lista cria o clássico "espaço sobrando na ponta" que precisa de :last-child pra consertar. gap só existe entre os itens. Uma propriedade, zero caso especial.

3.6 Grid: linhas e colunas ao mesmo tempo

Grid organiza em duas dimensões. É a ferramenta do layout da página e das malhas de cards:

.malha-indicadores {
  display: grid;
  grid-template-columns: repeat(4, 1fr);  /* 4 colunas iguais */
  gap: var(--espaco-3);
}

O vocabulário essencial:

.layout {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-areas:
    "menu topo"
    "menu conteudo";
}
.menu     { grid-area: menu; }
.topo     { grid-area: topo; }
.conteudo { grid-area: conteudo; }

E a linha mais valiosa do grid pra prova:

.malha-cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--espaco-3);
}

Tradução: "cria quantas colunas couberem, cada uma com no mínimo 220px, dividindo o resto igual". A malha de cards vira responsiva sem nenhuma media query: 4 colunas no monitor, 2 no tablet, 1 no celular, sozinha.

🛠️Gambiarra boa

🛠️ Gambiarra boa: decorar o repeat(auto-fit, minmax(220px, 1fr)) como uma frase. É um terço da responsividade da prova em uma linha.

3.7 Flexbox ou Grid? Os dois, cada um no seu lugar

A dúvida clássica tem resposta curta: flexbox é unidimensional (o conteúdo dita o tamanho, os itens se acomodam num eixo), grid é bidimensional (o layout dita a malha, o conteúdo entra nas células). Eles não competem; se compõem: grid desenha a página, flex alinha por dentro.

O mapa de decisão do Wedding Pass:

Situação Ferramenta Por quê
Navbar (logo + menu + usuário) flex uma linha, distribuir e alinhar
Linha de botões do form flex um eixo, alinhar à direita
Interior do card (nome + badge) flex um eixo
Centralizar o login na tela flex center nos dois eixos
Malha de indicadores do dashboard grid colunas e linhas de verdade
Página com menu lateral grid áreas nomeadas
Formulário em duas colunas grid campos alinhando em malha

Pra fixar a diferença, o mesmo dashboard resolvido nos dois. Com grid (a escolha certa):

.malha-indicadores {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--espaco-3);
}

Com flex (dá, mas se nota o remendo):

.malha-indicadores { display: flex; flex-wrap: wrap; gap: var(--espaco-3); }
.malha-indicadores .card { flex: 1 1 220px; }

O flex quebra linha, mas a última linha estica os cards que sobraram (3 cards numa linha de 4 ficam mais largos que os de cima, desalinhado com a malha). O grid mantém as colunas travadas. Regra de bolso final: se você quer que as linhas e colunas se alinhem entre si, é grid; se cada item pode ter seu tamanho, é flex.

🛠️Gambiarra boa

🛠️ Gambiarra boa: não sofrer com posicionamento antigo (float, margens negativas, position: absolute pra layout). Flexbox resolve os alinhamentos e Grid resolve as malhas. position: absolute fica pro que realmente flutua: badge em cima de card, toast no canto da tela (seção 6).

3.8 Hierarquia visual, cor e tipografia

O olho precisa saber pra onde olhar primeiro. Isso se cria com três alavancas:

Tamanho e peso (a escala tipográfica). Poucos tamanhos, sempre os mesmos:

Papel Tamanho Peso
Título da tela (h1) 1.75rem 700
Título de seção (h2) 1.25rem 600
Título de card (h3) 1rem 600
Corpo 1rem 400
Texto de apoio 0.875rem 400, cor suave
Número de indicador 2.5rem 700

E uma fonte só, a do sistema, que não precisa baixar nada e fica boa em qualquer máquina:

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
  color: var(--cor-texto);
  background: var(--cor-fundo);
  line-height: 1.5;
}

Cor com propósito. Uma cor principal pra ação (todo botão primário, todo link: a mesma), tons neutros pro resto, e cores de status (verde confirmou, vermelho erro, amarelo atenção). Lembra da avaliação por dedução de cores do módulo 01: usar poucas cores é zero; uma paleta bem resolvida com 4+ usos coerentes é pontuação total. O :root da seção 3.4 já é essa paleta.

Contraste que se lê. Texto sobre fundo precisa de contraste mínimo 4.5:1 (texto grande, 3:1). Cinza claro sobre branco reprova. O DevTools mostra a razão de contraste ao inspecionar qualquer texto (e o módulo 06 volta nisso com o olhar de acessibilidade).

⚠️Pega-ratão

⚠️ Pega-ratão: o "arco-íris". Cada botão de uma cor, texto em cinco tamanhos aleatórios, tudo competindo por atenção. Menos é mais: uma paleta enxuta e consistente parece muito mais profissional que um monte de cor. Isso é literalmente critério pontuado.

3.9 Movimento: transition, transform e o spinner

Movimento sutil é polimento barato. Dois usos e só:

Micro-resposta em hover e foco (150 a 200ms, nunca mais que 300ms em interação):

.botao-primario {
  transition: background-color 150ms ease, transform 100ms ease;
}
.botao-primario:hover  { background: var(--cor-acao-hover); }
.botao-primario:active { transform: scale(0.98); }

.card { transition: box-shadow 150ms ease; }
.card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); }

O spinner de carregamento (vai servir os estados da seção 5.5):

@keyframes girar { to { transform: rotate(360deg); } }

.spinner {
  width: 24px;
  height: 24px;
  border: 3px solid var(--cor-borda);
  border-top-color: var(--cor-acao);
  border-radius: 50%;
  animation: girar 0.8s linear infinite;
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: transition de 150ms nos botões e cards é o acabamento com melhor custo-benefício da prova: quatro linhas de CSS e a interface inteira parece "viva". Já animação chamativa (coisas voando, pulsando, girando sem motivo) joga contra: distrai e parece enfeite de quem não terminou o resto.


4. Responsividade e mobile-first

O descritivo pede interface responsiva: que funcione bem em telas de tamanhos diferentes, do celular ao monitor. A UC15 inteira é sobre isso.

Mobile-first na prática: o CSS base descreve a tela pequena (uma coluna, tudo empilhado), e as media queries adicionam colunas conforme o espaço aparece:

/* base: celular, uma coluna, sem media query nenhuma */
.malha-indicadores {
  display: grid;
  gap: var(--espaco-3);
}

/* tablet: duas colunas */
@media (min-width: 768px) {
  .malha-indicadores { grid-template-columns: repeat(2, 1fr); }
}

/* desktop: quatro colunas */
@media (min-width: 1024px) {
  .malha-indicadores { grid-template-columns: repeat(4, 1fr); }
}

Por que essa ordem (e não o contrário com max-width): a versão simples é a base, e complexidade só entra quando cabe. Esquecer um breakpoint deixa a tela empilhada porém funcional, nunca quebrada. Dois breakpoints bastam pra prova: 768px e 1024px.

O kit completo da responsividade:

  1. Meta viewport no HTML (seção 2.1). Sem ela, nada disso liga.
  2. Media queries min-width nos dois breakpoints.
  3. repeat(auto-fit, minmax(...)) nas malhas de cards (seção 3.6): responsivo sem media query.
  4. Imagem que não estoura: img { max-width: 100%; height: auto; } no reset.
  5. Tabela em tela pequena: tabela não espreme; ela rola pro lado dentro de um invólucro:
.tabela-wrapper { overflow-x: auto; }
<div class="tabela-wrapper">
  <table> ... </table>
</div>

(Alternativa mais elegante: em telas pequenas, mostrar cards em vez da tabela. Mais trabalho; o wrapper resolve em 30 segundos.)

  1. Alvos de toque: botão com padding generoso (mínimo ~44px de altura clicável) pra dedo, não só pra mouse.
🎯Foco de prova

🎯 Foco de prova: responsividade é ponto que a banca testa fácil, é só apertar a janela do navegador. Se a tela quebra (texto sai pra fora, botão some, tabela estoura a página), cai nota na hora. Testar em tela pequena durante o desenvolvimento, não no fim.

🛠️ Gambiarra boa: DevTools com o modo device (Ctrl+Shift+M) aberto num canto enquanto desenvolve, simulando um celular de ~375px. Cada tela nova, um olhar de 10 segundos: cabe, rola, aperta? Consertar na hora custa um minuto; descobrir na apresentação custa nota.


5. JavaScript: do clique até a API

Aqui os dois mundos se juntam. O front pede dados pra API (módulo 04) e mostra na tela. O descritivo diz assíncrono: a página não trava enquanto espera a resposta.

5.1 DOM e eventos: a rampa

O DOM é a página viva na memória do navegador; o JS lê e mexe nela. O ciclo de toda tela é: achar elemento → reagir a evento → atualizar a tela.

// achar elementos (uma vez, no topo do arquivo)
const form  = document.querySelector('#form-convidado');
const lista = document.querySelector('#lista-convidados');

// reagir ao envio do formulário
form.addEventListener('submit', (evento) => {
  evento.preventDefault(); // impede o recarregamento da página
  const dados = Object.fromEntries(new FormData(form));
  // dados = { nome: '...', email: '...', telefone: '...', acompanhantes: '0' }
  salvarConvidado(dados);
});
⚠️Pega-ratão

⚠️ Pega-ratão: esquecer o preventDefault. O sintoma é fantasmagórico: a página "pisca", a requisição morre pela metade, às vezes o dado até salva mas a tela não mostra. Se a tela pisca ao enviar, é isso.

Pra escrever na tela, o caminho da prova é gerar HTML com template literals (crases) e innerHTML. Com uma regra de segurança inegociável:

// escapa texto vindo do usuário antes de pôr no HTML
function escapar(texto) {
  const div = document.createElement('div');
  div.textContent = texto ?? '';
  return div.innerHTML;
}

lista.innerHTML = `<h3>${escapar(convidado.nome)}</h3>`;
⚠️Pega-ratão

⚠️ Pega-ratão: XSS, a irmã do SQL injection. innerHTML com dado do usuário sem escapar executa o que vier: um convidado cadastrado como <script>...</script> roda na tela da recepção. Mesmo raciocínio do módulo 04 (§6): dado de usuário nunca é código. Lá, prepared statement; aqui, escapar() em todo campo que veio de gente.

5.2 Síncrono, assíncrono e promises

Buscar dados na API leva tempo (rede). Se o JS esperasse parado, a página congelaria: sem clique, sem rolagem, sem digitação. Por isso toda operação de rede é assíncrona: o JS dispara o pedido, segue a vida, e trata a resposta quando ela chegar.

O objeto que representa "uma resposta que ainda vai chegar" é a Promise. Ela tem três estados: pendente (esperando), resolvida (deu certo, tem valor) ou rejeitada (deu erro). Dá pra consumir de dois jeitos:

// jeito antigo: encadear .then()
fetch(url)
  .then((resposta) => resposta.json())
  .then((dados) => mostrar(dados))
  .catch((erro) => avisar(erro.message));

// jeito da prova: async/await (lê de cima pra baixo, como código normal)
async function carregar() {
  try {
    const resposta = await fetch(url);
    const dados = await resposta.json();
    mostrar(dados);
  } catch (erro) {
    avisar(erro.message);
  }
}

São equivalentes. O async/await ganha porque parece código síncrono (fácil de ler e de debugar) e o erro cai num try/catch normal. Regras do jogo: await só dentro de função async, e todo await esquecido devolve uma Promise em vez do valor (se aparecer [object Promise] na tela, faltou um await).

5.3 A anatomia do fetch

fetch é a função do navegador pra chamar a API. Os dois formatos que cobrem a prova inteira, batendo nas rotas que vocês escreveram no módulo 04 (§2.4):

GET (buscar dados):

const resposta = await fetch('http://localhost:3000/convidados', {
  headers: { 'Authorization': `Bearer ${token}` },
});
const convidados = await resposta.json();

POST/PUT (enviar dados):

const resposta = await fetch('http://localhost:3000/convidados', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${token}`,
  },
  body: JSON.stringify({ nome, email, telefone, acompanhantes }),
});

As quatro peças: URL (a rota do mapa de endpoints), method (GET é o padrão; POST/PUT/DELETE se declara), headers (o Content-Type avisando que vai JSON + o token do módulo 04 §8), body (o objeto passado por JSON.stringify, nunca cru).

⚠️Pega-ratão

⚠️ Os três esquecimentos do fetch (decorar como checklist mental): 1. Esqueceu Content-Type: application/json → o back recebe body vazio. É exatamente o sintoma req.body undefined do módulo 04: agora você conhece os dois lados dele. 2. Esqueceu JSON.stringify → o body vira a string [object Object]. 3. Esqueceu o header Authorization → 401 em tudo, mesmo logada.

E o quarto primo: se o navegador reclamar de CORS no console, o problema é no back (módulo 04 §10), não no front. Não perder 40 minutos mexendo no fetch.

5.4 Tratamento de erro: o que o fetch NÃO faz por você

A pegadinha mais importante do módulo: o fetch só rejeita a Promise em erro de rede (API fora do ar, sem internet). Resposta 404, 409, 500? Pra ele, "deu certo": chegou uma resposta. Quem não sabe disso escreve try/catch, acha que está protegida, e os erros do servidor passam batidos pela tela.

A verificação é sua, via resposta.ok (verdadeiro só pra status 2xx):

async function buscarConvidados() {
  const resposta = await fetch(url, { headers });

  if (!resposta.ok) {
    // lê o corpo de erro padronizado do módulo 04: { erro, detalhes }
    const corpo = await resposta.json().catch(() => ({}));
    throw new Error(corpo.erro || `Erro ${resposta.status}`);
  }

  return resposta.json();
}

Dois caminhos de falha, dois comportamentos:

🎯Foco de prova

🎯 Foco de prova: a banca vai derrubar o caminho feliz: parar a API, cadastrar duplicado, buscar id inexistente (é literalmente o roteiro de Postman do módulo 04 §15, agora visto da tela). Front nota 3 é o que mostra mensagem clara nesses momentos em vez de tela travada em "Carregando..." pra sempre.

5.5 Os quatro estados de toda tela

Toda tela que consome API vive em um de quatro estados, e os quatro precisam existir no código:

  1. Carregando: esperando a resposta (spinner ou "Carregando...").
  2. Sucesso: chegaram dados, mostra.
  3. Vazio: deu certo, mas não tem nada ("Nenhum convidado cadastrado ainda"). Sucesso sem tratamento de vazio vira tela em branco confusa.
  4. Erro: falhou, avisa com mensagem legível e, ideal, um jeito de tentar de novo.

Os quatro implementados, no padrão que serve pra qualquer lista da prova:

async function carregarConvidados() {
  lista.innerHTML = '<div class="spinner"></div>';                       // 1. carregando

  try {
    const convidados = await api('/convidados');                          // seção 5.6

    if (convidados.length === 0) {                                        // 3. vazio
      lista.innerHTML = '<p class="estado-vazio">Nenhum convidado cadastrado ainda.</p>';
      return;
    }

    lista.innerHTML = convidados.map(cardConvidado).join('');             // 2. sucesso
  } catch (erro) {                                                        // 4. erro
    lista.innerHTML = `
      <p class="estado-erro">
        Não deu pra carregar a lista: ${escapar(erro.message)}
        <button type="button" onclick="carregarConvidados()">Tentar de novo</button>
      </p>`;
  }
}
🎯Foco de prova

🎯 Os estados que todo mundo esquece. Fazer só o caso de sucesso é nível 2. Tratar os quatro é nível 3. E é fácil esquecer o de erro porque, quando você testa, dá tudo certo. O teste honesto: desligar a API e abrir cada tela. Toda tela precisa reagir com dignidade. 30 segundos de teste, e é exatamente o que a banca faz.

5.6 O helper api(): escreva o fetch uma vez

Repetir aquele fetch de 15 linhas em cada chamada é convite pra esquecer um header e criar bug. O padrão profissional (e o maior economizador de tempo do Módulo C) é centralizar tudo num helper:

// js/api.js: toda a conversa com o back passa por aqui
const API_URL = 'http://localhost:3000';

async function api(rota, opcoes = {}) {
  const token = localStorage.getItem('token');

  const resposta = await fetch(API_URL + rota, {
    ...opcoes,
    headers: {
      'Content-Type': 'application/json',
      ...(token && { 'Authorization': `Bearer ${token}` }),
    },
    body: opcoes.body ? JSON.stringify(opcoes.body) : undefined,
  });

  // 401 = não autenticado (sem token, vencido ou inválido) → volta pro login
  if (resposta.status === 401) {
    localStorage.removeItem('token');
    localStorage.removeItem('usuario');
    window.location.href = 'index.html';
    return new Promise(() => {}); // página vai trocar; ninguém mais precisa da resposta
  }

  const corpo = resposta.status === 204 ? null : await resposta.json().catch(() => null);

  if (!resposta.ok) {
    // formato padronizado do módulo 04 (§12): { erro, detalhes }
    const erro = new Error(corpo?.erro || `Erro ${resposta.status}`);
    erro.status = resposta.status;
    erro.detalhes = corpo?.detalhes;
    throw erro;
  }

  return corpo;
}

E cada tela vira uma linha por chamada:

const convidados = await api('/convidados');
const novo = await api('/convidados', { method: 'POST', body: dados });
await api(`/convidados/${id}`, { method: 'PUT', body: dados });
await api(`/convidados/${id}/checkin`, { method: 'POST' });
const capacidade = await api(`/casamentos/${casamentoId}/capacidade`);

O que esse helper resolve de uma vez por todas:

🛠️Gambiarra boa

🛠️ Gambiarra boa: o api.js se escreve nos primeiros 10 minutos do Módulo C, junto com o :root do CSS. É o par de fundações. Vale treinar até sair de memória: são ~30 linhas que transformam toda integração do resto da prova em chamadas de uma linha.


6. Feedback visual: a interface conversando

O descritivo pede, no check-in, feedback visual imediato. Isso é um princípio geral: toda ação do usuário merece uma resposta visível. Três padrões implementados cobrem a prova inteira:

1. O toast (aviso que aparece e some). Serve pra confirmar qualquer ação: salvou, editou, apagou.

function avisar(mensagem, tipo = 'sucesso') {
  const aviso = document.createElement('div');
  aviso.className = `toast toast-${tipo}`;
  aviso.textContent = mensagem;
  document.body.append(aviso);
  setTimeout(() => aviso.remove(), 3500);
}

// uso: avisar('Convidado salvo!');  avisar(erro.message, 'erro');
.toast {
  position: fixed;
  top: var(--espaco-3);
  right: var(--espaco-3);
  padding: var(--espaco-3) var(--espaco-4);
  background: var(--cor-superficie);
  border-left: 4px solid var(--cor-sucesso);
  border-radius: var(--raio);
  box-shadow: var(--sombra);
}
.toast-erro { border-left-color: var(--cor-erro); }

2. O botão que mostra que está trabalhando. Durante o await, o botão trava e avisa:

form.addEventListener('submit', async (evento) => {
  evento.preventDefault();
  const botao = form.querySelector('button[type="submit"]');

  botao.disabled = true;
  botao.textContent = 'Salvando...';
  try {
    await api('/convidados', { method: 'POST', body: lerFormulario() });
    avisar('Convidado salvo!');
    form.reset();
  } catch (erro) {
    avisar(erro.message, 'erro');
  } finally {
    botao.disabled = false;
    botao.textContent = 'Salvar convidado';
  }
});
⚠️Pega-ratão

⚠️ Pega-ratão: o clique duplo. Botão sem disabled durante o await deixa o usuário clicar duas vezes e cadastrar dois convidados iguais (ou dois check-ins: a corrida do módulo 04 §11.2, provocada pela própria tela). O banco segura com a UNIQUE, mas a experiência fica feia e a banca vê. disabled = true na primeira linha, finally devolvendo, sempre.

3. O resultado gigante do check-in. A tela vitrine merece o feedback mais visível do sistema:

<div id="resultado-checkin" class="resultado" hidden></div>
async function fazerCheckin(convidadoId, nome) {
  const painel = document.querySelector('#resultado-checkin');
  try {
    await api(`/convidados/${convidadoId}/checkin`, { method: 'POST' });
    painel.className = 'resultado resultado-ok';
    painel.textContent = `✔ Bem-vinda, ${nome}!`;
  } catch (erro) {
    // o 409 do módulo 04: check-in duplicado
    painel.className = 'resultado resultado-erro';
    painel.textContent = `✖ ${erro.message}`;
  }
  painel.hidden = false;
}
.resultado {
  padding: var(--espaco-5);
  border-radius: var(--raio);
  font-size: 2rem;
  font-weight: 700;
  text-align: center;
  color: #fff;
}
.resultado-ok   { background: var(--cor-sucesso); }
.resultado-erro { background: var(--cor-erro); }
🎯Foco de prova

🎯 O check-in ágil é vitrine. O descritivo destaca "check-in ágil com feedback visual". É uma das telas que a banca mais olha. Feedback grande, colorido e imediato ali não é enfeite, é o critério: uma recepcionista real precisa bater o olho de longe e saber se pode deixar entrar. Verde gigante com o nome, ou vermelho gigante com o motivo. Fonte de 2rem pra cima, sem medo.


7. As telas da prova, com cara de nota 3

O descritivo nomeia telas específicas. O que cada uma cobra e como se resolve:

7.1 Login: a porta

Simples, mas se não funciona, nada funciona. O fluxo completo:

form.addEventListener('submit', async (evento) => {
  evento.preventDefault();
  try {
    const { token, usuario } = await api('/auth/login', {
      method: 'POST',
      body: Object.fromEntries(new FormData(form)),
    });

    localStorage.setItem('token', token);
    localStorage.setItem('usuario', JSON.stringify(usuario));

    // cada perfil cai direto na sua tela de trabalho
    window.location.href = usuario.perfil === 'recepcao' ? 'checkin.html' : 'dashboard.html';
  } catch {
    mostrarErro('E-mail ou senha inválidos');  // mensagem no form, nunca alert()
  }
});

A resposta do login é a que o módulo 04 (§8.2) definiu: { token, usuario: { id, nome, perfil } }. Guardar os dois no localStorage: o token pras chamadas, o usuário pra tela saber o nome e o perfil sem decodificar nada.

Detalhes que somam: input de senha com type="password", erro mostrado dentro do form perto dos campos, botão em "Entrando..." durante o await, e o redirecionamento por perfil (a recepção cai direto no check-in, que é onde ela vive).

7.2 Dashboard gerencial: números que respiram

Indicadores de lotação em tempo real: quantos confirmaram, quantos já entraram, quanto da capacidade foi usada, e o alerta de 90%. Os números vêm da rota de capacidade que vocês escreveram no módulo 04 (§11.1); o front só apresenta:

async function carregarIndicadores() {
  const dados = await api(`/casamentos/${casamentoId}/capacidade`);
  // os nomes dos campos são os que a rota de vocês devolve; o padrão do treino:
  // { capacidade_total, confirmados, presentes, percentual, alerta }

  document.querySelector('#num-confirmados').textContent = dados.confirmados;
  document.querySelector('#num-presentes').textContent = dados.presentes;
  document.querySelector('#barra-ocupacao').style.width = `${dados.percentual}%`;
  document.querySelector('#alerta-90').hidden = !dados.alerta;
}

carregarIndicadores();
setInterval(carregarIndicadores, 15000); // atualiza sozinho a cada 15s

A malha de cards usa o grid da seção 3.6, os números em 2.5rem peso 700 (a escala da 3.8), e o alerta de 90% é um banner que só perde o hidden quando a rota mandar alerta: true: fundo --cor-alerta, texto direto ("⚠ Capacidade acima de 90%").

🎯Foco de prova

🎯 "Tempo real" na prática. Não precisa de tecnologia complicada. Basta a tela buscar o número atualizado quando abre e quando algo muda (recarregar os indicadores depois de cada check-in confirmado). O setInterval de 15s é a gambiarra honesta que fecha o resto. O que a banca quer ver é o número refletindo a realidade do banco: ela faz um check-in e olha o dashboard; se o número andou, é tempo real o suficiente. WebSocket é bala de canhão pra esse pardal.

7.3 Check-in ágil: a vitrine

O fluxo da recepcionista tem que caber em segundos: digitar um pedaço do nome (ou o código do convite), ver o convidado, confirmar, feedback gigante. Componentes já construídos nesse módulo: busca com a lista filtrando enquanto digita (seção 9.3), botão de confirmar com disabled durante o await (seção 6), painel verde/vermelho de resultado (seção 6). O erro 409 de dupla entrada vem do back com a mensagem pronta (erro.message), o front só exibe no vermelho.

Um detalhe de agilidade que a banca sente: o campo de busca já nasce focado (autofocus no input, ou campo.focus() ao carregar). A recepcionista não pega o mouse.

7.4 Os três perfis na interface

Os perfis são os do ENUM do módulo 03: admin, organizador e recepcao. Cada um enxerga um sistema diferente:

Perfil O que vê
admin tudo: dashboard, convidados (com excluir), mesas, check-in
organizador dashboard, convidados e mesas (sem excluir convidado)
recepcao busca + check-in (a tela dela é o sistema inteiro)

Como esconder e mostrar é a seção 8. O que importa aqui: a experiência de cada perfil precisa fazer sentido sozinha: a recepção não deve nem ver menu pra telas que vão dar 403.


8. Controle de acesso na interface

Duas camadas no front, as duas em js/auth.js, importado por toda página interna:

1. A guarda de página (as primeiras linhas do arquivo): sem token, nem carrega a tela:

// js/auth.js
if (!localStorage.getItem('token')) {
  window.location.href = 'index.html';
}
const usuario = JSON.parse(localStorage.getItem('usuario') || 'null');

2. Render condicional por perfil: o HTML marca o que é restrito com classes, e uma linha de JS limpa o que não é do perfil:

<button class="botao-perigo so-admin" data-acao="excluir">Excluir</button>
<a href="dashboard.html" class="so-gestao">Dashboard</a>
if (usuario.perfil !== 'admin') {
  document.querySelectorAll('.so-admin').forEach((el) => el.remove());
}
if (usuario.perfil === 'recepcao') {
  document.querySelectorAll('.so-gestao').forEach((el) => el.remove());
}

remove() em vez de esconder com CSS: o elemento sai do DOM de verdade (não fica um botão "invisível" que um Ctrl+F no inspetor acha em 5 segundos).

⚠️Pega-ratão

⚠️ Pega-ratão (o mesmo de sempre, do outro lado): achar que esconder o botão é segurança. Não é. Esconder é experiência (não confundir o usuário com opções que ele não pode usar). A segurança é o back barrar com 403 (módulo 04 §9), porque o token dá pra editar, o JS dá pra desligar e a rota dá pra chamar por Postman. Os dois trabalham juntos: front esconde pra ficar limpo, back barra pra ficar seguro. Na apresentação, falar essa frase vale ponto de julgamento.


9. SPA, componentes e estado

O descritivo cita Full Stack / SPA (Single Page Application): navegação sem recarregar a página, trocando só o conteúdo. Antes da arquitetura, os dois conceitos que valem em qualquer front:

9.1 Componente: a função que rende HTML

Um componente, em vanilla JS, é só uma função que recebe dados e devolve HTML:

function cardConvidado(convidado) {
  const status = convidado.status_convite ?? 'pendente';
  return `
    <article class="card" data-id="${convidado.id}">
      <div class="card-topo">
        <h3>${escapar(convidado.nome)}</h3>
        <span class="badge badge-${status}">${status}</span>
      </div>
      <p class="texto-suave">
        Mesa ${convidado.mesa_numero ?? 'sem mesa'} · ${convidado.acompanhantes} acompanhante(s)
      </p>
      <div class="acoes">
        <button type="button" data-acao="editar">Editar</button>
        <button type="button" class="so-admin" data-acao="excluir">Excluir</button>
      </div>
    </article>`;
}

// uma lista inteira é um map + join:
lista.innerHTML = convidados.map(cardConvidado).join('');
🛠️Gambiarra boa

🛠️ Gambiarra boa: componentizar economiza tempo e nota. O cardConvidado aparece na lista, no resultado da busca do check-in e no detalhe. Escrito uma vez. Menos código, menos bug, e consistência visual automática (todos os cards idênticos = tela profissional). É trabalho no começo que paga no meio da prova, quando o tempo aperta.

Os cliques nos botões dos cards usam delegação: um listener na lista resolve todos os cards, inclusive os que ainda vão nascer:

lista.addEventListener('click', (evento) => {
  const botao = evento.target.closest('[data-acao]');
  if (!botao) return;
  const id = botao.closest('.card').dataset.id;
  if (botao.dataset.acao === 'editar') abrirEdicao(id);
  if (botao.dataset.acao === 'excluir') confirmarExclusao(id);
});

(Listener em cada card se perde toda vez que o innerHTML re-renderiza. Delegação na lista sobrevive.)

9.2 Estado: os dados que a tela mostra

Estado é o que a tela está exibindo agora: a lista de convidados carregada, o texto digitado na busca. O padrão que organiza qualquer tela da prova: um objeto de estado + uma função render() que desenha a partir dele:

const estado = { convidados: [], busca: '' };

function render() {
  const filtrados = estado.convidados.filter((c) =>
    c.nome.toLowerCase().includes(estado.busca.toLowerCase())
  );
  lista.innerHTML = filtrados.length
    ? filtrados.map(cardConvidado).join('')
    : '<p class="estado-vazio">Nenhum convidado encontrado.</p>';
}

9.3 A regra de ouro: mudou o estado, chama o render

// carregou da API → estado muda → render
estado.convidados = await api('/convidados');
render();

// digitou na busca → estado muda → render (busca instantânea, sem ir ao servidor)
campoBusca.addEventListener('input', () => {
  estado.busca = campoBusca.value;
  render();
});

A busca local filtrando a cada tecla dá a sensação de agilidade que o check-in pede. (Pra listas grandes, a API do módulo 04 §13 já aceita ?busca=; com o volume da prova, filtrar o que já veio é mais rápido e mais simples.)

9.4 E o SPA?

Multi-página (um HTML por tela) é o caminho recomendado: mais simples, F5 sempre funciona, menos risco. Se a prova exigir SPA explícito, a versão vanilla honesta: um HTML só com uma <section> por tela, navegação mostrando uma e escondendo as outras:

function navegar(tela) {
  document.querySelectorAll('main > section').forEach((s) => (s.hidden = true));
  document.querySelector(`#tela-${tela}`).hidden = false;
}

O resto (componentes, estado, render) é idêntico. É por isso que essa seção veio antes: quem domina componente + estado troca de arquitetura em 20 minutos.


10. Validação no client: resposta imediata

O sistema valida em três camadas, e cada uma tem um papel:

  1. Atributos HTML (required, type, minlength, min/max): de graça, sem JS.
  2. JS antes do fetch: mensagens melhores, no lugar certo, na hora.
  3. O servidor (módulo 04 §5): a única que é segurança. As duas primeiras são experiência.
⚠️Pega-ratão

⚠️ Pega-ratão conceitual (de novo, porque a banca pergunta): validação só no front é porta destrancada, o Postman passa reto por ela. O front valida pra responder na hora sem viagem ao servidor; o back valida pra proteger. As duas existem, com mensagens parecidas, e você sabe explicar por quê.

A validação de JS no submit, usando os espaços de erro do form da seção 2.3:

function validarConvidado(dados) {
  const erros = {};
  if (!dados.nome || dados.nome.trim().length < 3) {
    erros.nome = 'Informe o nome completo (mínimo 3 letras)';
  }
  if (dados.email && !dados.email.includes('@')) {
    erros.email = 'E-mail inválido';
  }
  const acomp = Number(dados.acompanhantes);
  if (Number.isNaN(acomp) || acomp < 0 || acomp > 5) {
    erros.acompanhantes = 'Acompanhantes: entre 0 e 5';
  }
  return erros;
}

form.addEventListener('submit', async (evento) => {
  evento.preventDefault();
  const dados = Object.fromEntries(new FormData(form));

  const erros = validarConvidado(dados);
  mostrarErrosNoForm(erros);                 // escreve em cada .campo-erro
  if (Object.keys(erros).length > 0) return; // não chama a API com dado inválido

  // ... segue pro api() com botão travado (seção 6)
});

function mostrarErrosNoForm(erros) {
  form.querySelectorAll('.campo').forEach((campo) => {
    const input = campo.querySelector('input, select');
    const aviso = campo.querySelector('.campo-erro');
    const mensagem = erros[input.name];
    aviso.textContent = mensagem ?? '';
    aviso.hidden = !mensagem;
    input.classList.toggle('campo-invalido', Boolean(mensagem));
  });
}
.campo-erro { color: var(--cor-erro); font-size: 0.875rem; }
.campo-invalido { border-color: var(--cor-erro); }
🎯Foco de prova

🎯 Onde a nota mora: mensagem perto do campo, em português claro, dizendo o que fazer ("Informe o nome completo", não "erro no formulário"). E se o back devolver detalhes no erro (o formato do módulo 04 §12), mostrar essas mensagens também: o helper api() já as entrega em erro.detalhes.

E as máscaras de telefone e afins? São a cereja da lapidação e têm módulo próprio: o padrão formata-mostra/guarda-limpo está no módulo 08, com código pronto. Aqui, o essencial: máscara é conforto visual; validação é outra coisa; e campo mascarado se manda limpo pra API.


11. Peso da página: otimização que dá pra ver

A UC15 cobra "otimização para dispositivos móveis". Na prova isso se traduz em: a página abre rápido e nada trava. O checklist enxuto:

  1. Imagem é o vilão número 1. Foto de fundo de 4000px no login = página lenta na frente da banca. Antes de usar: redimensionar pro tamanho real de exibição e comprimir (Squoosh faz os dois no navegador). Formato: foto em JPG/WebP, ícone e logo em SVG ou PNG pequeno.
  2. loading="lazy" em imagem que aparece em lista (só carrega quando entra na tela): uma palavra no HTML.
  3. Fonte de sistema (seção 3.8): zero download de fonte. A página mais rápida é a que não pede.
  4. Sem biblioteca desnecessária: cada framework/plugin que "ajudaria" custa peso, risco e tempo de aprender na hora. Vanilla resolve a prova inteira: é a decisão que esse módulo inteiro treina.
  5. Minificação (saber explicar): remover espaços e comentários do CSS/JS pra reduzir bytes. Em produção real, ferramenta de build; na prova, não vale o tempo. O que vale: um CSS e poucos JS (menos requisições), e saber dizer o que minificação é se a banca perguntar (a UC15 cita).
  6. Conferir no DevTools, aba Network: recarregar e olhar o total de KB e o tempo. Se a página passa de uns poucos MB, tem imagem gorda escondida.
🛠️Gambiarra boa

🛠️ Gambiarra boa: a otimização com melhor retorno na prova são imagens tratadas + zero dependência. Se a página abre instantânea no notebook da banca, otimização cumprida. Lighthouse (no próprio DevTools) dá um relatório com nota se sobrar tempo de curiosidade no treino.


12. Autoavaliação do módulo

  1. Qual o papel de HTML, CSS e JavaScript, e por que separar os três (em papéis e em arquivos)?
  2. O que a meta viewport faz? O que acontece com as media queries sem ela?
  3. Por que button de verdade ganha de div com clique? Cita duas coisas que a tag dá de graça.
  4. O que label for + id faz e por que é o ponto de acessibilidade mais barato do form?
  5. Numa disputa entre .card .titulo e .titulo, quem ganha e por quê? Qual a estratégia de especificidade da prova e por que !important é armadilha?
  6. O que box-sizing: border-box muda na conta da largura? Quais as três linhas do reset mínimo?
  7. rem ou px: qual usar em fonte e espaçamento, e por quê?
  8. Por que o :root com custom properties é a primeira coisa que se escreve no CSS da prova? Que ligação ele tem com o módulo 06?
  9. Flexbox ou grid: qual o critério de escolha? Dá um exemplo do Wedding Pass de cada.
  10. O que repeat(auto-fit, minmax(220px, 1fr)) faz e por que economiza media query?
  11. O que é mobile-first e quais os dois breakpoints do treino? Como uma tabela sobrevive à tela pequena?
  12. O que o fetch NÃO faz quando o servidor responde 500? O que é resposta.ok e por que checar?
  13. Quais os quatro estados de uma tela que consome API? Qual o teste de 30 segundos que revela se estão tratados?
  14. Cita três problemas que o helper api() resolve de uma vez. Qual a diferença de tratamento entre 401 e 403 no front?
  15. Por que innerHTML com dado de usuário sem escapar é perigoso? Qual o paralelo com o módulo 04?
  16. Por que o botão trava (disabled) durante o await? Que bug do módulo 04 isso evita de provocar?
  17. Se o back já valida, por que validar no front também? O que cada camada garante?
  18. De onde vem o número de "lotação em tempo real" e o que "tempo real" exige de fato na prova?
  19. Duas otimizações de página com melhor retorno na prova: quais e por quê?
💡Nota

Front no lugar? Agora a gente aprofunda no que faz uma interface ser boa de usar, não só bonita: 06 — UX e interface.


Referências do módulo

Bibliografia do plano de curso (UC12 e UC15): - SILVA, Maurício Samy. CSS Grid Layout. Novatec. - DUCKETT, Jon. HTML e CSS: projete e construa websites. Alta Books. - ZEMEL, Tárcio. CSS eficiente: técnicas e ferramentas que fazem a diferença nos seus estilos. Casa do Código. - LOPES, Sérgio. A Web Mobile: programe para um mundo de muitos dispositivos. Casa do Código. - SOUZA, Weiller. Bootstrap 4: conheça o framework front-end mais utilizado no mundo (leitura de contexto; a prova é vanilla).

Documentação e mercado: - MDN: conceitos básicos de flexbox - MDN: grids CSS - MDN: usando propriedades personalizadas (custom properties) - MDN: media queries - MDN: usando a Fetch API - MDN: async e await - web.dev: tratamento de erros com a Fetch API - Dmitri Pavlutin: How to Use Fetch with async/await - Alura: quando utilizar Grid e quando utilizar Flexbox - Alura: praticando CSS com Grid e Flexbox - Le Wagon: CSS Grid ou Flexbox - Impacta: Flexbox ou CSS Grid, quando utilizar cada ferramenta

Competições Senac RS · Seletiva 26

Módulo 06

Módulo 06 — UX e interface

BaseUC16 do plano de curso (Aplicar UX no desenvolvimento web mobile, 60h)Peso na prova10%NaturezaRevisão no básico, Novo no aprofundamento
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. UX vale 10% direto, mas é diferente dos outros: não é uma tela, é uma camada de qualidade que atravessa o front inteiro. Muito do que separa nível 2 de nível 3 no front (módulo 05) é, no fundo, UX. Por isso esse módulo rende duas vezes: pontua nos 10% dele e empurra os 40% do front pra cima. No cronograma ele tem um dia inteiro só dele (segunda 31/08), de propósito antes da semana do front: primeiro se decide como as telas vão ser, depois se coda. Na apostila ele vem depois do 05 porque dá nome e método ao que o front construiu. O autofocus do check-in, o painel gigante verde/vermelho, os quatro estados de tela: tudo isso tem teoria por trás, e a diferença entre nível 2 e nível 3 é aplicar essa teoria de propósito, sabendo o nome do que se está fazendo.


1. UX não é o mesmo que "deixar bonito" 🔄

Três termos que se confundem e não são a mesma coisa:

Uma tela linda que confunde tem boa UI e péssima UX. Uma tela feia em que tudo funciona no primeiro clique tem UI fraca e UX razoável. Na prova, a meta é ter as duas, mas se um dia for preciso escolher, clareza ganha de beleza.

A usabilidade tem uma definição clássica de cinco componentes (Nielsen), e vale conhecer porque cada um vira uma pergunta concreta sobre o Wedding Pass:

Componente A pergunta No Wedding Pass
Facilidade de aprendizado Na primeira vez, a pessoa consegue? A Renata faz o primeiro check-in da vida dela sem ninguém explicar.
Eficiência Depois que aprendeu, é rápido? Autofocus + Enter: check-in em duas ações (05 §7.3).
Memorização Voltou uma semana depois, ainda sabe? Consistência entre telas: aprendeu uma, sabe todas.
Erros Dá pra errar? E dá pra se recuperar? Validação em três camadas + mensagem do 409 com horário.
Satisfação É agradável de usar? Feedback imediato em tudo, tela que nunca engasga.

Repara que as coisas boas que o módulo 05 construiu no código caem cada uma numa casinha dessa tabela. Isso não é coincidência: é o que esse módulo faz. Ele pega o que vocês já fazem por instinto e dá nome, critério e método.

🎯Foco de prova

🎯 A pergunta que resume o módulo: "a pessoa que vai usar isso consegue, sozinha, na primeira vez, sem instrução?" Se sim, a UX está boa. No Wedding Pass, a pessoa é uma recepcionista no dia do casamento, com fila na frente. Ela não tem tempo de pensar. A interface tem que ser óbvia.


2. As dez heurísticas de Nielsen, uma a uma no Wedding Pass 🆕

Heurísticas são regras práticas pra julgar se uma interface é boa. As dez de Jakob Nielsen (1994) são o padrão da indústria até hoje: aparecem em vaga de emprego, em curso, em auditoria de UX profissional e, com outras palavras, nos critérios de julgamento da banca.

Na Regional, a intuição de vocês bastava. Agora não basta mais: as dez entram com nome e endereço. Por três motivos:

  1. Elas viram roteiro de auto-teste: uma passada pelas dez no fim do Módulo C acha problema de graça (o 🛠️ no fim desta seção).
  2. Na apresentação individual (módulo D), citar a heurística pelo nome ao explicar uma decisão é um porquê pronto, e porquê é o que a banca quer ouvir.
  3. Os descritores de julgamento de UX são, na prática, as heurísticas reescritas. Quem conhece a régua joga o jogo da régua.

2.1 Visibilidade do estado do sistema (Visibility of system status)

O sistema sempre diz o que está acontecendo. O usuário nunca fica olhando pra tela sem saber se clicou, se está processando, se deu certo.

No Wedding Pass isso já tem forma concreta: os quatro estados de tela do 05 §5.5 (carregando, vazio, erro, sucesso) são exatamente essa heurística em código. O spinner no botão enquanto a API responde, o toast do avisar(), o painel gigante do check-in: tudo visibilidade de estado.

O dashboard com setInterval de 15 segundos (05 §7.2) é a versão ambiciosa: o estado do salão quase em tempo real, sem a Olívia apertar F5.

🛠️Gambiarra boa

🛠️ Gambiarra boa: "atualizado às 19h42". Uma linha de texto no canto do dashboard com a hora da última atualização. Custa um new Date().toLocaleTimeString() e muda a percepção do sistema inteiro: de "será que esses números são de agora?" pra "isso aqui está vivo". Detalhe barato que banca sente.

E tem um nível acima da tela: o alerta de 90% da capacidade (04 §11.1) é visibilidade de estado no nível do negócio. Não é só a tela dizendo o que ela está fazendo, é o sistema avisando do problema antes de o problema acontecer. Quando forem explicar esse alerta na apresentação, o nome disso é heurística 1.

2.2 Correspondência entre o sistema e o mundo real (Match between system and the real world)

O sistema fala a língua de quem usa, não a língua de quem programou.

⚠️Pega-ratão

⚠️ Pega-ratão: vazar linguagem de máquina. "FK constraint failed", "Error 500", "undefined" aparecendo pro usuário é quebra dupla: heurística 2 (língua errada) e heurística 9 (erro que não ajuda). O helper api() do 05 §5.6 existe pra isso: ele pega o { erro } que a API mandou e entrega uma frase em português de gente. Se em algum fluxo aparecer texto de máquina na tela, tem um buraco no funil de erros.

2.3 Controle e liberdade do usuário (User control and freedom)

Sempre tem saída. A pessoa entrou onde não queria? Volta. Abriu um modal sem querer? Fecha. Começou uma ação? Cancela.

Desfazer de verdade (o Ctrl+Z de sistema) é caro de implementar e a prova não pede. A confirmação bem feita é o substituto barato: em vez de deixar desfazer, impede de fazer sem querer.

2.4 Consistência e padrões (Consistency and standards)

O mesmo tipo de coisa se comporta do mesmo jeito em todo lugar. Botão de ação principal sempre da mesma cor. "Cancelar" sempre à esquerda de "confirmar". Mensagem de erro sempre no mesmo lugar. Consistência reduz carga mental: aprendeu uma tela, entende todas.

Existem duas consistências:

Os tokens do 05 §3.4 são a arma secreta aqui: quando só existe --cor-acao, não tem como o botão de salvar sair de outra cor. A consistência deixa de ser esforço e vira consequência. É por isso que os tokens se decidem no dia de UX (§8) e se codam no primeiro dia de CSS.

⚠️Pega-ratão

⚠️ Pega-ratão: cada tela com um padrão diferente (um botão salvar verde aqui, azul ali; um formulário que confirma, outro que não). Parece detalhe, mas passa desleixo e é sentido direto na avaliação de julgamento.

2.5 Prevenção de erros (Error prevention)

Melhor que uma boa mensagem de erro é o erro que nem consegue acontecer.

No Wedding Pass, em ordem de esforço:

🛠️Gambiarra boa

🛠️ Gambiarra boa: desabilitar o botão de enviar enquanto o formulário está incompleto. O usuário nem consegue errar. É uma linha de lógica que evita um monte de estado inválido e passa sensação de cuidado.

2.6 Reconhecimento em vez de memorização (Recognition rather than recall)

O usuário não deveria precisar decorar nada. As opções estão à vista, os campos têm rótulo claro, os ícones vêm com texto.

2.7 Flexibilidade e eficiência de uso (Flexibility and efficiency of use)

Aceleradores pra quem já sabe usar, sem atrapalhar quem está aprendendo.

Resultado: check-in completo em duas ações (digitar/colar o código + Enter). A Renata do §3 é a persona inteira dessa heurística: ela usa o sistema 200 vezes na mesma noite, cada clique economizado vale 200 cliques.

🎯Foco de prova

🎯 Como cai na prova: a demo do check-in rápido no módulo D é essa heurística em ação. "O check-in sai em dois segundos porque no dia do evento tem fila na recepção" é frase de nível 3: mostra a feature, a persona e a decisão de projeto numa tacada só.

2.8 Estética e design minimalista (Aesthetic and minimalist design)

Cada elemento na tela compete por atenção com todos os outros. Informação irrelevante não é neutra: ela dilui a relevante.

⚠️Pega-ratão

⚠️ Pega-ratão: minimalista não é vazio de features. A heurística não manda cortar funcionalidade, manda dar a cada tela um trabalho claro. Entregar tudo que o enunciado pede continua obrigatório. Poluição é o painel com 20 números; feature a mais bem organizada é ponto a mais.

2.9 Ajudar o usuário a reconhecer, diagnosticar e se recuperar de erros (Help users recognize, diagnose, and recover from errors)

Quando o erro acontece, a mensagem diz o que houve e como resolver, na língua do domínio (§2.2).

A anatomia da mensagem boa: o que aconteceu + o próximo passo. Compara:

Essa última é o 409 da corrida de check-in (04 §11.2) chegando na tela pelo painel vermelho do 05 §6. Banco, API e front alinhados numa frase.

🎯Foco de prova

🎯 Mensagem de erro boa é ouro de nível 3. É um dos lugares mais fáceis de subir de 2 pra 3, e um dos mais esquecidos. Enquanto todo mundo entrega "deu erro", quem entrega "faltou preencher o nome" já se destaca. O módulo 08 volta nisso com a biblioteca completa de mensagens do sistema.

2.10 Ajuda e documentação (Help and documentation)

A melhor interface não precisa de manual. Quando precisar de alguma ajuda, ela aparece no contexto, na hora da dúvida, não numa página separada.

Na prova, ajuda é microtexto:

⚠️Pega-ratão

⚠️ Não gastar tempo com página de Ajuda/FAQ. Ninguém pede, ninguém pontua, e o tempo dela faz falta. A heurística 10 na prova se cumpre inteira com microtextos bem colocados.

As dez numa tabela (o roteiro de auto-teste)

# Heurística No Wedding Pass O que a banca vê
1 Visibilidade do estado Quatro estados, toast, "atualizado às 19h42" A tela nunca fica muda
2 Sistema × mundo real Convidado/mesa/check-in, datas BR, badge "Pendente" Linguagem do domínio
3 Controle e liberdade Cancelar em tudo, Esc fecha modal, confirmação nomeada Ninguém fica preso
4 Consistência e padrões Tokens, Cancelar à esquerda, erro no mesmo lugar Aprendeu uma tela, entende todas
5 Prevenção de erros required/max, select de mesa, botão desabilitado Difícil errar
6 Reconhecimento > memorização "Mesa 3 (2/8)", placeholder com exemplo, cor E texto Ninguém decora nada
7 Flexibilidade e eficiência Autofocus, Enter, check-in em 2 ações A Renata voa
8 Minimalismo Dashboard com 4 números, check-in quase vazio Foco no que importa
9 Recuperação de erros "Já fez check-in às 19h32", erro com próximo passo Erro que ajuda
10 Ajuda e documentação Placeholder, texto de apoio, empty state que ensina Não precisa de manual
🛠️Gambiarra boa

🛠️ Gambiarra boa: a avaliação heurística de 15 minutos. Avaliação heurística é a técnica profissional de revisar uma interface passando as heurísticas uma a uma. Na prova ela vira isso: nos últimos 15 minutos do Módulo C, abrir cada tela e descer a lista das dez perguntando "onde isso está aqui?". Campo sem label, tela sem estado de carregando, erro genérico, botão inconsistente: cada furo achado nesses 15 minutos é nota que não se perde. É a revisão mais barata da prova, e é exatamente o que a banca vai fazer depois, só que com caneta.


3. Personas e mapa de empatia enxutos 🆕

Persona é uma pessoa concreta, inventada, que representa um perfil real de uso. Não é enfeite de slide: é ferramenta de decisão. Quando duas soluções de tela empatam, a persona desempata: qual das duas ajuda mais essa pessoa, nesse momento, com essa pressa?

O Wedding Pass tem três perfis com login (o ENUM do banco: admin, organizador, recepcao) e um ator sem login (o convidado que responde o RSVP, módulo 07). Quatro personas, um mapa de empatia enxuto pra cada:

Renata, da recepção (perfil recepcao)

Olívia, a organizadora (perfil organizador)

Ada, a admin (perfil admin)

Caio, o convidado (sem login)

O mapa de empatia completo da literatura tem mais quadrantes (o que a pessoa fala, faz, ouve). Na prova, esses quatro campos (Vê / Sente / Precisa / Dor nº 1) carregam todo o valor e cabem num canto de folha.

🛠️Gambiarra boa

🛠️ Como usar na prova: 5 minutos, no rascunho, antes do wireframe. Escrever as personas com nome e dor. Parece perda de tempo e é o contrário: toda decisão de tela nas 4 horas seguintes fica mais rápida porque existe critério ("isso ajuda a Renata ou atrapalha?"). E na apresentação individual, citar a persona é um porquê pronto: "o campo já vem focado porque a recepção opera com fila" vale muito mais que "achei melhor assim". O módulo 10 monta o banco de porquês; as personas são fornecedoras oficiais dele.


4. Arquitetura da informação: organizar pra achar 🔄

Arquitetura da informação (AI) é como o conteúdo e a navegação se organizam: o usuário entende onde as coisas estão e como chegar nelas sem pensar.

Os três princípios da Regional continuam valendo:

O aprofundamento é transformar "caminho curto" em regra que se verifica: a tarefa nº 1 de cada perfil sai em no máximo 2 ações a partir do login.

Persona Tarefa nº 1 Caminho
Renata Fazer check-in Login → já cai na tela de check-in com o campo focado. Zero navegação.
Olívia Ver/cadastrar convidados Login → "Convidados" é o primeiro item do menu dela.
Ada Gerenciar usuários Login → "Usuários" direto no menu.

Repara que o redirect por perfil (05 §7.1) e o menu por perfil (05 §8) são exatamente isso: arquitetura da informação aplicada. Cada perfil enxerga um mapa do tamanho do trabalho dele, e o sistema já abre no lugar certo. Não foi capricho de código, foi decisão de AI.

Ferramenta prática: o inventário de telas. Antes de desenhar qualquer coisa, listar as telas que o sistema tem (login, convidados + formulário, check-in, dashboard, e a página pública de RSVP quando o módulo 07 chegar) e, pra cada uma: quem usa, qual a tarefa principal, de onde se chega, pra onde se vai. Meia página que vira o esqueleto do wireflow do §5.

🎯Foco de prova

🎯 Pensar no fluxo de cada perfil. A Renata vive na tela de check-in. A Olívia vive entre convidados e dashboard. A arquitetura da informação boa leva cada perfil direto pro que ele mais usa. Isso conecta com os perfis de acesso do módulo 04 e com a guarda de página do módulo 05.


5. Wireframe e protótipo rápido: a segunda 31/08 🆕

Wireframe é o rascunho estrutural da tela. Ele existe pra decidir layout e fluxo antes de gastar tempo codando, e pra errar barato: apagar um retângulo de caneta custa 5 segundos, refazer uma tela em CSS custa 40 minutos.

O que o wireframe TEM:

O que o wireframe NÃO tem: cor, fonte, ícone caprichado, sombra. Se tem cor, virou desenho artístico e perdeu a função: a discussão vira "que verde bonito" em vez de "por que a busca está embaixo?".

Fidelidade: baixa (papel e caneta) contra alta (Figma e afins). Na prova, papel, sempre. Três motivos: é mais rápido, não tenta enfeitar, e o rascunho não é entregável, é ferramenta de pensar. Ninguém pontua o wireframe; pontua-se a tela que saiu certa de primeira por causa dele.

As quatro telas do Wedding Pass, conteúdo mínimo de cada

Wireflow: as telas ligadas

Wireframe mostra a tela; wireflow mostra a viagem. As quatro telas na mesa, setas numeradas ligando: de onde vem, pra onde vai, o que dispara cada passagem. A seta que sai do login se divide em três, uma por perfil (o redirect do 05 §7.1 desenhado no papel antes de existir em código).

Teste de mesa: o protótipo de papel funcionando

Com os wireframes prontos, o teste mais barato do mundo: uma narra, a outra opera. Uma faz a Renata ("cheguei, tem fila, o convidado me deu o código ABC12345") e a outra aponta com o dedo, no papel, onde clica e o que acontece. Toda hesitação do dedo é um problema de tela achado de graça, antes de existir uma linha de código.

Os tokens no canto da folha

Última coisa da segunda-feira: anotar no canto do wireframe as decisões de estilo (§8): a cor de ação, as quatro cores de status, a escala de espaço, o raio. Na quarta-feira da semana do front, essas anotações viram o :root do 05 §3.4, sem discussão e sem inventar na hora.

🛠️Gambiarra boa

🛠️ Gambiarra boa: 5 minutos de rascunho salvam 40 de retrabalho. Antes de sair fazendo a tela no código, um rabisco rápido no papel (o que vai onde, qual o fluxo do check-in) evita descobrir no meio que o layout não fecha. Na prova, esse rascunho é o que faz o front sair direto, sem refação. Pensar a tela é mais barato que refazer a tela.

⚠️ Pega-ratão: tratar o wireframe como contrato eterno. Se no meio do código uma decisão se mostrar ruim, muda. O wireframe é ferramenta, não promessa. O crime não é mudar o rascunho, é começar sem rascunho.

🔄→🆕 O protocolo da segunda 31/08. Manhã: personas (§3), inventário de telas (§4), wireframes das quatro telas e wireflow (§5). Tarde: avaliação heurística do próprio wireframe (§2), Gestalt e hierarquia aplicadas no papel (§6 e §7), tokens decididos e anotados (§8). A semana do front pega tudo pronto: terça o HTML nasce dos wireframes, quarta os tokens viram :root. Nenhuma tela é inventada com o VS Code aberto.


6. Gestalt: por que uma tela parece organizada 🆕

Antes de ler qualquer palavra, o olho já agrupou a tela. Gestalt é o conjunto de regras desse agrupamento automático. Quem conhece as regras desenha telas que o olho entende sozinho; quem não conhece depende de sorte.

As cinco que resolvem interface, cada uma com a tradução em CSS:

🎯Foco de prova

🎯 Como cai na prova: quando a banca escreve "organizado", "limpo" ou "profissional" na avaliação, ela está sentindo Gestalt sem usar o nome. Vocês aplicando com o nome ganham duas vezes: a tela sai organizada e a explicação no módulo D sai pronta ("os campos de contato têm menos espaço entre si do que pro resto do formulário: proximidade agrupa").


7. Hierarquia visual: as alavancas 🔄🆕

Hierarquia visual é dizer pro olho o que olhar primeiro. As alavancas são cinco, e toda tela boa usa várias ao mesmo tempo:

A regra 60-30-10: 60% da tela em cor neutra (fundos), 30% em estrutura (textos, bordas, superfícies), 10% em cor de ação. É o que faz o botão primário gritar: ele é praticamente a única coisa colorida da tela. Se a cor de ação está em todo lugar, não existe ação em lugar nenhum.

Cor semântica fixa: verde = confirmado/sucesso, vermelho = recusado/erro/perigo, amarelo = pendente/atenção. São os tokens de status do 05 §3.4. Regra dura: essas cores nunca aparecem fora do significado. Um título verde decorativo quebra o código de cores do sistema inteiro.

Scanning em F: em interface, ninguém lê, todo mundo escaneia: linhas horizontais no topo, depois uma descida rápida pela margem esquerda. Duas consequências práticas: a primeira palavra do rótulo carrega o sentido ("Confirmar check-in", nunca "Clique aqui para confirmar o check-in"), e a coluna mais importante da tabela fica à esquerda (nome do convidado primeiro, ações por último).

Contraste tem número: 4.5:1 pra texto normal, 3:1 pra texto grande (WCAG). Os exemplos práticos e a checagem no DevTools estão no 05 §3.8; o que pertence a este módulo é a decisão: escolher combinações de texto e fundo que passam antes de codar, no dia dos tokens. Contraste não se conserta no fim, se decide no início.


8. Design system mínimo: decidir uma vez, usar sempre 🆕

Design system de empresa tem centenas de componentes documentados e um time cuidando. O da prova cabe num canto de folha e se decide uma vez, na segunda 31/08:

O CSS disso já existe: é o bloco :root do 05 §3.4. A divisão de trabalho entre os módulos é essa: aqui é onde ele é decidido, lá é onde ele vive. Token não é técnica de CSS, é decisão de design congelada em código. Quando a decisão vem antes, o CSS vira digitação.

A segunda metade do design system é o inventário de componentes: a lista fechada de peças com que todas as telas são montadas.

Componente Variações Estados Onde aparece
Botão primário, secundário, perigo normal, hover, desabilitado, carregando todas as telas
Input com label texto, e-mail, senha, select normal, foco, erro login, formulário de convidado
Card métrica, formulário — dashboard, convidados
Badge de status pendente, confirmado, recusado, presente — tabela, dashboard
Toast sucesso, erro entra, sai sozinho todas (o avisar() do 05 §6)
Painel de resultado verde, vermelho — check-in
Confirmação destrutiva — — excluir convidado
Tabela — carregando, vazia, cheia, erro convidados

(No celular, a tabela rola horizontal dentro do próprio container, 05 §4. Os estados da linha da tabela são os quatro estados do 05 §5.5.)

Oito componentes. Todas as telas do Wedding Pass são combinações dessas oito peças com os mesmos tokens. É pouca coisa de propósito: pouco e consistente ganha de muito e bagunçado, sempre.

🎯Foco de prova

🎯 Como cai na prova: o que a banca chama de "aparência profissional" é exatamente isto: nada fora do sistema. Toda tela usando os mesmos oito componentes com os mesmos tokens parece produto de verdade; cada tela inventando um botão novo parece trabalho de escola. A nota de julgamento sente essa diferença mesmo sem nomear.

🛠️ Gambiarra boa: tokens de memória. Decorar os próprios tokens durante o treino (a cor de ação, os quatro status, a escala, o raio). Na prova, o :root sai em 10 minutos sem nenhuma decisão nova, e todas as telas nascem consistentes por construção. É o truque que faz 4 horas de Módulo C renderem como 6.


9. Acessibilidade que pontua 🔄

Acessibilidade é fazer a interface funcionar pra mais gente: quem usa leitor de tela, quem enxerga pouco, quem não usa mouse, quem está num celular ruim no sol. E o melhor argumento pra prova: quase tudo que é acessível é melhor pra todo mundo. A Renata no tablet com pressa se beneficia dos mesmos cuidados que um usuário de leitor de tela.

Os oito itens baratos que pontuam:

  1. Contraste: os números do §7 (4.5:1 e 3:1), decididos junto com os tokens. Checagem: DevTools ou o WebAIM Contrast Checker (referências).
  2. Label ligado ao campo: <label for="email"> casando com o id do input (05 §2.3). Clicar no rótulo foca o campo, leitor de tela anuncia o nome certo. Custo zero.
  3. Foco visível: nunca outline: none sem substituto. Com :focus-visible dá pra trocar o outline padrão por um contorno na cor de ação: fica bonito e acessível.
  4. Navegação por teclado: Tab percorre os elementos na ordem do DOM (mais um motivo pro HTML em ordem lógica, 05 §2). Teste concreto: fazer um check-in inteiro sem tocar no mouse. No Wedding Pass isso nem é acessório: é o fluxo real da Renata.
  5. Semântica: button é <button>, link é <a>. O leitor de tela entende button, não entende div clicável.
  6. Alt em imagem: informativa descreve ("Logo do Wedding Pass"), decorativa leva alt="" vazio de propósito, pro leitor de tela pular em vez de ler o nome do arquivo.
  7. Alvo de toque: 44 a 48px de altura pra botão e link no mobile. (O mínimo formal do WCAG 2.2 é 24px; dedo de verdade, com pressa, agradece o dobro.) Botão pequeno no tablet da recepção é erro de digitação em série.
  8. Erro junto do campo, com cor E texto: borda vermelha + mensagem embaixo do campo que falhou. Cor sozinha exclui quem não distingue cor; mensagem longe do campo obriga a caçar o problema.
🛠️Gambiarra boa

🛠️ Verificação de 1 minuto: Tab pela tela inteira (o foco está sempre visível? a ordem faz sentido?) e depois DevTools > Lighthouse > Accessibility. O relatório aponta contraste ruim, campo sem label e alt faltando, de graça. Cabe dentro dos 15 minutos da avaliação heurística (§2).

🎯 Acessibilidade pontua e é boa prática de mercado. Não precisa ser perfeita na prova, mas contraste bom, formulário com rótulo e foco visível são baratos e mostram maturidade. É o tipo de coisa que dev júnior ignora e dev bom faz no automático.


10. Microcopy: as palavras da interface 🆕

As palavras da interface são interface. Um botão mal nomeado é um bug de UX igual a um layout quebrado, só que mais barato de consertar.

A regra de ouro pra botão: verbo + objeto. O botão diz o que acontece quando é apertado.

Fraco Nota 3
OK Confirmar check-in
Erro Não achei nenhum convite com esse código. Confere os 8 caracteres.
Sucesso! Check-in de Maria Silva confirmado às 19h32.
Excluir? Excluir Maria Silva? Os acompanhantes dela saem da lista junto. Essa ação não tem volta.
(lista vazia em branco) Nenhum convidado ainda. Clica em "Novo convidado" pra cadastrar o primeiro.

As regras por trás da coluna da direita:

O módulo 08 volta nisso com a biblioteca completa de mensagens do sistema, prontas pra reusar. Aqui o que importa é o critério: cada texto da interface passa no teste "isso ajuda a pessoa a decidir o que fazer agora?".


11. Como o CIS enxerga UX 🎯

UX é quase toda avaliação por julgamento (0 a 3), porque é qualitativa. Onde a nota aparece:

O que a banca sente Como subir pra nível 3
A interface é fácil de entender sem explicação? Rótulos claros, fluxo óbvio, consistência (§2.4, §2.6).
O sistema dá retorno das ações? Quatro estados + toast + painel em tudo (§2.1).
As mensagens de erro ajudam? O que houve + como resolver, na língua do domínio (§2.9, §10).
A interface é consistente? Design system mínimo aplicado sem exceção (§8).
Dá pra usar bem no celular? Mobile-first de verdade (05 §4) + alvo de toque (§9).
A aparência é profissional? Tokens + Gestalt + hierarquia (§6, §7, §8).
Tem cuidado com acessibilidade? Os oito itens baratos do §9.

E como um descritor de julgamento de UX tipicamente escala, pra vocês saberem em que degrau cada entrega está:

A diferença entre 2 e 3 é o nosso jogo inteiro, e nesse critério ela é feita de detalhes que custam minutos. Poucos pontos da prova são tão baratos por unidade de esforço.

⚠️Pega-ratão

⚠️ Pega-ratão: tratar UX como "depois eu deixo bonito". UX não é a última demão de tinta, é decisão que se toma desde o wireframe. Feedback, consistência e clareza se constroem junto com a tela, não coladas no fim.

🎯 Módulo D: cada decisão desse módulo é um porquê pronto. "Por que o painel do check-in é gigante?" tem resposta em duas camadas: a persona (a Renata decide de longe, com fila) e a heurística (visibilidade do estado). Decisão de UX citada com nome de técnica é o material do banco de porquês do módulo 10.


12. Autoavaliação do módulo

  1. Qual a diferença entre UX, UI e usabilidade? Dá um exemplo de boa UI com péssima UX.
  2. Quais são os cinco componentes da usabilidade? Qual deles o autofocus do check-in melhora mais?
  3. Sem olhar a tabela: quantas das dez heurísticas você cita de cabeça, cada uma com um exemplo do Wedding Pass?
  4. "Registro inserido com sucesso" quebra qual heurística? Reescreve a mensagem pro padrão nota 3.
  5. O que é uma avaliação heurística e em que momento da prova ela entra?
  6. Quem são Renata, Olívia, Ada e Caio? Qual a dor nº 1 de cada?
  7. Por que o alerta de 90% da capacidade é uma decisão de UX, e não só uma regra de negócio?
  8. A tarefa nº 1 de cada perfil sai em quantas ações a partir do login? Por que isso é arquitetura da informação?
  9. O que um wireframe tem e o que ele não tem? Por que texto realista importa?
  10. Como funciona o teste de mesa com o protótipo de papel?
  11. Explica a proximidade (Gestalt) usando a escala de espaço do módulo 05.
  12. O que é a regra 60-30-10 e qual token é os 10%?
  13. Por que decorar os próprios tokens no treino economiza tempo na prova?
  14. Cita três cuidados de acessibilidade baratos e o que o Lighthouse verifica por você.
  15. Reescreve pra nota 3: o botão "OK", uma lista vazia em branco e a mensagem "Erro".
  16. UX vale 10% da prova. Por que ele influencia bem mais que 10% da nota final?

💡Nota

UX na cabeça? Agora a gente junta tudo nas funcionalidades que são novas em relação à Regional: 07 — Funcionalidades novas (Wedding Pass). Cada feature nova de lá termina numa tela, e cada tela passa pelo crivo deste módulo.


Referências do módulo

Do plano de curso (bibliografia da UC16):

Da web (mercado):

Competições Senac RS · Seletiva 26

Módulo 07

Módulo 07 — Funcionalidades novas (Wedding Pass)

BaseUC1 do plano de curso (Planejar desenvolvimento de software, 36h) + aplicação de tudo que veio nos módulos 03 a 06Peso na provanão tem peso próprio, aplica banco (10%), back (20%), front (40%) e UX (10%) juntosNaturezaNovo
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Por que esse módulo é diferente. Os módulos 02 a 06 revisaram e aprofundaram o que as duas já dominam. Esse aqui é onde a gente sai da Regional e sobe o nível. O cronograma reserva as semanas 2 e 3 de setembro pra isso: funcionalidades que não existiam no que elas fizeram na Regional, ou que existiam mais simples. Cada funcionalidade aqui é um exercício de juntar banco + back + front + UX numa coisa só que funciona ponta a ponta. É onde a teoria dos outros módulos vira sistema.

Tem um detalhe de ritmo que muda tudo: no treino, cada funcionalidade dessas ganha um ou dois dias. Na prova, várias delas juntas cabem em poucas horas. A ordem certa é exatamente essa: primeiro fazer bem, com calma, entendendo cada camada. Depois, na reta final, fazer rápido. Ninguém comprime o que ainda não domina. Esse módulo é o "fazer bem"; a seção 10 já começa a treinar o "fazer rápido".

Deixando claro (pra qualquer pessoa entender): o que está aqui é 🆕 novo em relação à Regional. Não é revisar o que elas já sabem, é construir capacidade nova em cima da base que elas já têm.


1. O método: de enunciado a funcionalidade em quatro passos 🆕

Antes de qualquer código, esse módulo ensina um método. É a habilidade que a UC1 chama de "planejar o desenvolvimento": pegar um pedaço de enunciado e transformar em plano concreto. Na Seletiva, isso é literalmente o Módulo A (Prova Surpresa, 2 horas): entregam um requisito que ninguém viu antes e esperam a feature funcionando. Quem tem método começa a codar em 20 minutos sabendo pra onde vai. Quem não tem, improvisa e refaz.

Os quatro passos, sempre na mesma ordem:

Passo Pergunta que ele responde O que sai dele
1. Requisito O que o enunciado exige de verdade? Frases grifadas, separadas em obrigatórias (must) e desejáveis (should)
2. Entidades Que dados isso precisa guardar? Um CREATE TABLE ou ALTER TABLE + dicionário rápido dos campos novos
3. Endpoints Que conversas front ↔ back isso cria? Linhas novas na tabela de rotas: verbo, URL, quem pode, status possíveis
4. Telas Onde o usuário faz isso e o que ele vê? Rascunho da tela + os quatro estados dela (módulo 05, seção 5.5)

A ordem importa. Dados antes de endpoints (o endpoint serve dado, então o dado precisa existir primeiro). Endpoints antes de telas (a tela consome o endpoint). É a mesma ordem de construção que vocês treinam desde o módulo 03: banco → back → front.

1.1 O método aplicado num exemplo real

Pega esse pedaço de enunciado, do jeito que ele apareceria na prova:

💡Nota

"O convidado deve poder responder ao convite através de um link, informando se comparecerá e quantos acompanhantes levará (no máximo 5). O total de pessoas confirmadas não pode ultrapassar a capacidade do evento. Seria desejável que o convidado visualizasse o prazo de resposta."

Passo 1 — Requisito. Grifando os verbos de obrigação:

Passo 2 — Entidades. O que já existe no banco do módulo 03: convite tem codigo e status, convidado tem acompanhantes com CHECK <= 5, casamento tem capacidade_total. Ou seja: nenhuma tabela nova. Só entra um ALTER TABLE pra datas e prazo (seção 3.2). Perceber que a modelagem já cobre o requisito é resultado do passo, não falta de trabalho: você acabou de economizar meia hora.

Passo 3 — Endpoints. Duas linhas novas na tabela de rotas:

Verbo URL Quem pode Status possíveis
GET /convites/:codigo Público 200, 404
POST /convites/:codigo/resposta Público 200, 404, 409, 422

Passo 4 — Telas. Uma página nova, rsvp.html, fora da área logada. Estados: carregando, convite não encontrado, formulário pronto, respondido com sucesso, erro de rede. Rascunho de 2 minutos no papel: nome do casal em cima, dados do convite, dois botões grandes ("Confirmo" / "Não vou"), seletor de acompanhantes.

Pronto. Isso é um plano. As seções 3 e 4 constroem exatamente essa feature, camada por camada.

🛠️Gambiarra boa

🛠️ Gambiarra boa: o ritual dos 10 minutos. Sempre que cair uma feature nova (no treino ou na prova), os quatro passos vão pro papel antes do VS Code abrir. Dez minutos, no máximo. Parece perda de tempo com o relógio correndo; é o contrário: o papel vira o mapa e elimina o "e agora?" no meio do caminho. Na Prova Surpresa, esse papel é o cronograma pessoal das 2 horas.

🎯 Como isso cai na prova. O Módulo A é esse método cronometrado. O enunciado da Surpresa vem curto e às vezes em inglês (módulo 09 treina a leitura). A banca avalia o resultado, mas o julgamento (0 a 3) enxerga o processo: uma feature com banco certo, regra no lugar certo e tela com estados é a assinatura de quem planejou. A seção 10 monta o timebox completo.


2. Os três perfis, agora de ponta a ponta 🆕

Na Regional eram dois perfis. O Wedding Pass tem três, e o módulo 03 já deixou isso pronto no banco: usuario.perfil ENUM('admin','organizador','recepcao'). O módulo 04 (seção 9) construiu a ferramenta (middleware de autorização). O que esse módulo faz é o trabalho de projeto: decidir, rota por rota, quem pode o quê, e blindar tudo.

Colocando as personas do módulo 06 nos papéis: a Ada é admin, a Olívia é organizadora, a Renata é da recepção, e o Caio é convidado (nem conta tem: ele só existe do lado público).

2.1 A matriz de autorização completa

Essa tabela é o coração do módulo. Ela é a verdade sobre permissões no sistema, e é o tipo de artefato que vale mostrar pra banca no Módulo D:

Rota Público Renata (recepção) Olívia (organizadora) Ada (admin)
POST /auth/login ✅ ✅ ✅ ✅
GET /convidados (+ ?busca=) ❌ ✅ ✅ ✅
POST /convidados ❌ ❌ ✅ ✅
PUT /convidados/:id ❌ ❌ ✅ ✅
DELETE /convidados/:id ❌ ❌ ❌ ✅
POST /convidados/:id/checkin ❌ ✅ ❌ ✅
POST /checkins (por código, seção 5) ❌ ✅ ❌ ✅
GET /casamentos/:id/capacidade ❌ ✅ ✅ ✅
GET /casamentos/:id/mesas ❌ ✅ ✅ ✅
GET /casamentos/:id/dashboard ❌ ❌ ✅ ✅
POST /convites (enviar convite, seção 3.5) ❌ ❌ ✅ ✅
GET /convites/:codigo ✅ ✅ ✅ ✅
POST /convites/:codigo/resposta ✅ ✅ ✅ ✅
GET/POST/PUT/DELETE /usuarios ❌ ❌ ❌ ✅

2.2 Escada como ponto de partida, matriz como verdade

O jeito natural de pensar três perfis é a escada: admin pode tudo do organizador, que pode tudo da recepção. A escada é ótima pra começar (organiza o raciocínio e evita esquecer rota sem proteção). Mas olha a matriz de novo: ela tem uma linha que quebra a escada.

O check-in é da Renata e da Ada. A Olívia não faz check-in. Se a escada fosse lei, organizadora poderia tudo que a recepção pode. Só que a operação da porta é um papel operacional do dia do evento, com responsabilidade própria (o registrado_por da tabela checkin diz quem deixou a pessoa entrar). A organizadora gerencia a lista; quem opera a porta é a recepção. Separar os dois é uma decisão de projeto defensável, e defensável é a palavra favorita do Módulo D.

A lição que fica: escada pra rascunhar, matriz pra decidir. E toda exceção da matriz precisa de um porquê escrito, porque é exatamente nas exceções que a banca pergunta "por que isso aqui é assim?".

⚠️Pega-ratão

⚠️ Pega-ratão: implementar a autorização como if (perfil !== 'admin') espalhado pelos controllers. Vira caça ao tesouro pra achar onde cada regra mora, e uma rota esquecida passa aberta. A matriz vive num lugar só: a definição das rotas.

2.3 A matriz virando código

Recapitulando a ferramenta do módulo 04 (seção 9): autenticar valida o token e põe o usuário no request (401 se não tem ou tá inválido); permitir(...) confere o perfil (403 se não pode). A matriz inteira vira uma linha por rota:

Node/Express:

// rotas/convidados.js — a matriz da seção 2.1 virando código, linha a linha
router.get('/', autenticar, listarConvidados); // qualquer perfil logado
router.post('/', autenticar, permitir('organizador', 'admin'), criarConvidado);
router.put('/:id', autenticar, permitir('organizador', 'admin'), editarConvidado);
router.delete('/:id', autenticar, permitir('admin'), removerConvidado);
router.post('/:id/checkin', autenticar, permitir('recepcao', 'admin'), fazerCheckin);

PHP:

// api/convidados.php — mesma matriz, roteamento manual
$usuario = exigir_login($pdo); // devolve 401 e encerra se o token nao vale

switch ("$metodo $recurso") {
    case 'GET convidados':
        // qualquer perfil logado passa
        listar_convidados($pdo);
        break;
    case 'POST convidados':
        exigir_perfil($usuario, ['organizador', 'admin']); // 403 se nao pode
        criar_convidado($pdo, $dados);
        break;
    case 'DELETE convidados':
        exigir_perfil($usuario, ['admin']);
        remover_convidado($pdo, $id);
        break;
}

Se os nomes das funções na API de vocês estiverem diferentes, sem problema. O contrato é o que importa: 401 pra quem não provou quem é, 403 pra quem provou mas não pode (módulo 04, seção 9 tem a distinção completa).

2.4 O front acompanha a matriz (mas não substitui ela)

O módulo 05 (seção 8) montou a guarda de página e o menu por perfil. Com três perfis, o mapa fica assim: login redireciona a Renata direto pro check-in (tela dela), a Olívia pro dashboard, a Ada pra onde quiser (menu completo). Botões que o perfil não pode usar nem aparecem (esconder é UX: menos ruído pra persona apressada do módulo 06).

E a frase que resume a divisão de trabalho continua valendo: front esconde, back barra. Esconder o botão de excluir não impede um DELETE disparado pelo Postman. Quem barra é o middleware.

2.5 O teste do token fraco (o favorito da banca)

O teste que a banca faz, e que vocês vão fazer antes dela:

  1. Loga com a Renata no Postman. Guarda o token dela.
  2. Chama DELETE /convidados/1 com esse token no header.
  3. Resposta esperada: 403 com o corpo padrão de erro ({ "erro": ... }, módulo 04 seção 12). Se vier 200, o controle é de fachada: o front escondia, mas o back não barrava.
  4. Repete pra cada ❌ da linha da Renata na matriz. Depois com o token da Olívia (o dela tem que levar 403 no check-in e no DELETE).

São uns 2 minutos por rota com o Postman organizado (módulo 04, seção 15: salva essas requisições numa pasta "testes de autorização" da collection). Rodar essa bateria depois de qualquer mexida nas rotas vira hábito.

🎯Foco de prova

🎯 Como isso cai na prova. Cada perfil vendo e fazendo só o que pode é critério objetivo (sim/não, rota a rota). O julgamento entra na qualidade das exceções: a banca pergunta por que a organizadora não faz check-in, e a resposta pronta ("papel operacional da porta, com auditoria própria") é diferença entre 2 e 3. Guarda essa resposta no banco de porquês do Módulo D (módulo 10).


3. O fluxo convite → RSVP → follow-up 🆕

Essa é a jornada central do Wedding Pass, e é a funcionalidade mais completa do sistema: começa com a Olívia enviando um convite, passa pelo Caio respondendo por um link público, e volta pra Olívia acompanhar quem ainda não respondeu. Um fluxo, três atores, as três camadas envolvidas o tempo todo.

⚠️Pega-ratão

⚠️ Pega-ratão (o mesmo da v1 do sistema de vocês, e continua valendo): tratar convite, confirmação e acompanhamento como três coisas soltas. Elas são um fluxo: o mesmo convidado, o mesmo código, mudando de estado. Modelar como ciclo de estados, não como telas desconectadas, é o que faz o sistema parecer inteiro.

3.1 O ciclo de estados

Todo convite vive nesse ciclo:

             ┌──── sim ────▶  confirmado  ──── check-in ────▶  presente
pendente ────┤                                (linha na tabela checkin)
             └──── não ────▶  recusado

Três decisões de projeto escondidas nesse desenho:

  1. Os estados moram no banco, não na tela. convite.status é o ENUM do módulo 03 (pendente, confirmado, recusado). "Presente" não é um quarto valor do ENUM: é a existência de uma linha em checkin. Por quê? Porque check-in tem dados próprios (quando entrou, quem registrou) e uma constraint própria (a UNIQUE que bloqueia dupla entrada). Estado com dado junto vira tabela, não coluna.
  2. Transições são validadas no back. Responder um convite que não está pendente é 409. Fazer check-in duas vezes é 409. O front mostra bonito, mas quem impede transição ilegal é a API.
  3. Cada transição carimba um horário: respondido_em no convite, registrado_em no checkin. Auditoria barata que alimenta o follow-up e o dashboard de graça.

3.2 O código único (a "senha" do convidado)

Cada convite tem um código de 8 caracteres, único no sistema. Ele é o que o Caio recebe no link: quem tem o código, responde o convite. Isso o torna, na prática, a senha do convidado. Duas consequências:

Não pode ser adivinhável. Se o código fosse o id sequencial (/convites/1, /convites/2...), qualquer pessoa trocaria o número na URL e responderia o convite dos outros. Enumerável = quebrado.

Não pode ser ambíguo. O código vai ser lido em voz alta na porta do evento e digitado pela Renata. Então o alfabeto tira os caracteres que se confundem: sem 0/O, sem 1/I/l.

Node:

const crypto = require('crypto');

// 32 simbolos, sem os ambiguos (0/O, 1/I/l)
const ALFABETO = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';

function gerarCodigo(tamanho = 8) {
    let codigo = '';
    for (const byte of crypto.randomBytes(tamanho)) {
        codigo += ALFABETO[byte % ALFABETO.length];
    }
    return codigo;
}

PHP:

function gerar_codigo(int $tamanho = 8): string {
    $alfabeto = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    $codigo = '';
    foreach (str_split(random_bytes($tamanho)) as $byte) {
        $codigo .= $alfabeto[ord($byte) % strlen($alfabeto)];
    }
    return $codigo;
}

Nos dois casos a fonte é o gerador criptográfico do sistema (crypto.randomBytes / random_bytes), não Math.random()/rand(), que são previsíveis.

E se dois códigos colidirem? Com 32 símbolos e 8 posições são 32⁸ combinações (mais de 1 trilhão): colisão é quase impossível, mas "quase" não é argumento de engenharia. A rede de segurança é a constraint UNIQUE no convite.codigo (módulo 03): se colidir, o INSERT falha, e o código tenta de novo:

// o UNIQUE do banco e a rede; o retry e o trapezista se levantando
for (let tentativa = 1; tentativa <= 3; tentativa++) {
    try {
        const codigo = gerarCodigo();
        await conexao.execute(
            'INSERT INTO convite (convidado_id, codigo) VALUES (?, ?)',
            [convidadoId, codigo]
        );
        return codigo;
    } catch (erro) {
        if (erro.code !== 'ER_DUP_ENTRY') throw erro;
        // colidiu (raro demais): gera outro e tenta de novo
    }
}
throw new Error('Nao consegui gerar um codigo unico');

Detalhe fino: ER_DUP_ENTRY aqui também dispara se o convidado já tem convite (a UNIQUE de convidado_id). Por isso o fluxo real do POST /convites (seção 3.5) confere antes se já existe convite pro convidado: se existe, é reenvio, não convite novo.

A migração desse módulo. O fluxo precisa de três campos que o banco do módulo 03 ainda não tem:

ALTER TABLE convite
    ADD COLUMN enviado_em    DATETIME NULL,
    ADD COLUMN respondido_em DATETIME NULL,
    ADD COLUMN prazo_resposta DATE     NULL;
🛠️Gambiarra boa

🛠️ Gambiarra boa: o arquivo migrations.sql. Banco evolui por ALTER, não por DROP + recriar (dropar mata os dados de teste que vocês cadastraram, e na prova mata o seed). Todo ALTER novo entra no fim de um arquivo migrations.sql versionado no Git. Além de ser prática real de mercado, é evidência linda pra banca: o histórico mostra o banco evoluindo junto com as features.

3.3 Os endpoints públicos do RSVP

Aqui aparece uma categoria nova de rota: pública de verdade. O Caio não tem login; o código no link é a credencial dele. As duas rotas ficam fora do middleware autenticar.

Buscar o convite (o que a página pública carrega):

// rotas/publico.js — sem autenticar: o codigo E a credencial
router.get('/convites/:codigo', async (req, res, next) => {
    try {
        const [linhas] = await pool.execute(
            `SELECT c.codigo, c.status, c.prazo_resposta,
                    cv.nome AS convidado, cv.acompanhantes,
                    ca.nome AS casamento, ca.data_evento, ca.local
               FROM convite c
               JOIN convidado cv ON cv.id = c.convidado_id
               JOIN casamento ca ON ca.id = cv.casamento_id
              WHERE c.codigo = ?`,
            [req.params.codigo]
        );
        if (linhas.length === 0) {
            return res.status(404).json({ erro: 'Convite não encontrado' });
        }
        res.json(linhas[0]);
    } catch (erro) { next(erro); }
});

Responder o convite (o endpoint mais denso do sistema). Ele valida entrada, confere estado, confere prazo, e aplica a regra de capacidade dentro de uma transação. Vale ler com calma porque ele reúne meio módulo 04 num lugar só:

router.post('/convites/:codigo/resposta', async (req, res, next) => {
    const { resposta, acompanhantes = 0 } = req.body;

    // 1. Validar a entrada (422 antes de tocar no banco)
    const detalhes = [];
    if (!['confirmado', 'recusado'].includes(resposta)) {
        detalhes.push('resposta deve ser "confirmado" ou "recusado"');
    }
    if (!Number.isInteger(acompanhantes) || acompanhantes < 0 || acompanhantes > 5) {
        detalhes.push('acompanhantes deve ser um número inteiro entre 0 e 5');
    }
    if (detalhes.length > 0) {
        return res.status(422).json({ erro: 'Dados inválidos', detalhes });
    }

    const conexao = await pool.getConnection();
    try {
        await conexao.beginTransaction();

        // 2. Travar o convite e conferir o estado atual
        const [convites] = await conexao.execute(
            `SELECT c.id, c.status, c.prazo_resposta,
                    cv.id AS convidado_id, cv.casamento_id
               FROM convite c
               JOIN convidado cv ON cv.id = c.convidado_id
              WHERE c.codigo = ?
              FOR UPDATE`,
            [req.params.codigo]
        );
        if (convites.length === 0) {
            await conexao.rollback();
            return res.status(404).json({ erro: 'Convite não encontrado' });
        }
        const convite = convites[0];

        if (convite.status !== 'pendente') {
            await conexao.rollback();
            return res.status(409).json({ erro: 'Convite já respondido' });
        }
        if (convite.prazo_resposta && new Date(convite.prazo_resposta) < new Date()) {
            await conexao.rollback();
            return res.status(409).json({ erro: 'Prazo de resposta encerrado' });
        }

        // 3. Se for "sim", conferir a capacidade com a linha do casamento travada
        if (resposta === 'confirmado') {
            const [casamentos] = await conexao.execute(
                `SELECT ca.capacidade_total,
                        COALESCE((SELECT SUM(1 + cv2.acompanhantes)
                                    FROM convidado cv2
                                    JOIN convite c2 ON c2.convidado_id = cv2.id
                                   WHERE cv2.casamento_id = ca.id
                                     AND c2.status = 'confirmado'), 0) AS ja_confirmados
                   FROM casamento ca
                  WHERE ca.id = ?
                  FOR UPDATE`,
                [convite.casamento_id]
            );
            const { capacidade_total, ja_confirmados } = casamentos[0];
            if (Number(ja_confirmados) + 1 + acompanhantes > capacidade_total) {
                await conexao.rollback();
                return res.status(409).json({ erro: 'Capacidade do evento esgotada' });
            }
        }

        // 4. Efetivar as duas escritas e commitar
        await conexao.execute(
            'UPDATE convite SET status = ?, respondido_em = NOW() WHERE id = ?',
            [resposta, convite.id]
        );
        await conexao.execute(
            'UPDATE convidado SET acompanhantes = ? WHERE id = ?',
            [acompanhantes, convite.convidado_id]
        );
        await conexao.commit();

        res.json({ mensagem: 'Resposta registrada', status: resposta });
    } catch (erro) {
        await conexao.rollback();
        next(erro);
    } finally {
        conexao.release();
    }
});

O que o FOR UPDATE está serializando aqui. Imagina o evento com um lugar sobrando e dois convidados clicando "Confirmo" no mesmo segundo. Sem trava, os dois SELECTs contam a mesma ocupação, os dois passam na regra, os dois confirmam: capacidade estourada. Com o FOR UPDATE na linha do casamento, o segundo request fica esperando o primeiro commitar; quando ele finalmente conta, a confirmação do primeiro já está lá, e ele leva o 409. É a mesma lógica de condição de corrida do módulo 04 (seção 11.2), com outra roupa: lá o UPDATE atômico resolvia numa instrução só; aqui a decisão precisa de conta no meio, então trava-se a linha-pai, conta, decide. Guarda esse molde: travar a linha-pai, contar, decidir. Ele volta duas vezes nesse módulo.

Sobre o prazo estourado: a resposta 409 com mensagem clara resolve. Existe um status mais fino pra "isso existia e expirou" (410 Gone); citar ele na apresentação mostra repertório, mas usar 409 padronizado é decisão perfeitamente defensável.

A versão PHP do mesmo endpoint (mesmo raciocínio, sintaxe PDO; as consultas SQL são idênticas):

// publico/resposta.php
$pdo->beginTransaction();
try {
    $busca = $pdo->prepare(
        'SELECT c.id, c.status, c.prazo_resposta,
                cv.id AS convidado_id, cv.casamento_id
           FROM convite c JOIN convidado cv ON cv.id = c.convidado_id
          WHERE c.codigo = ? FOR UPDATE'
    );
    $busca->execute([$codigo]);
    $convite = $busca->fetch();

    if (!$convite) { $pdo->rollBack(); responder(404, ['erro' => 'Convite não encontrado']); }
    if ($convite['status'] !== 'pendente') { $pdo->rollBack(); responder(409, ['erro' => 'Convite já respondido']); }

    if ($resposta === 'confirmado') {
        $cap = $pdo->prepare('...mesma consulta de capacidade do Node, com FOR UPDATE...');
        $cap->execute([$convite['casamento_id']]);
        $c = $cap->fetch();
        if ($c['ja_confirmados'] + 1 + $acompanhantes > $c['capacidade_total']) {
            $pdo->rollBack();
            responder(409, ['erro' => 'Capacidade do evento esgotada']);
        }
    }

    $pdo->prepare('UPDATE convite SET status = ?, respondido_em = NOW() WHERE id = ?')
        ->execute([$resposta, $convite['id']]);
    $pdo->prepare('UPDATE convidado SET acompanhantes = ? WHERE id = ?')
        ->execute([$acompanhantes, $convite['convidado_id']]);
    $pdo->commit();

    responder(200, ['mensagem' => 'Resposta registrada', 'status' => $resposta]);
} catch (Throwable $e) {
    $pdo->rollBack();
    responder(500, ['erro' => 'Erro interno']);
}

(responder() é o helper de resposta JSON do módulo 04: seta o http_response_code, imprime o JSON e encerra.)

3.4 A página pública rsvp.html

A página que o Caio abre pelo link rsvp.html?codigo=A7XK2MQP. Primeira coisa: ler o código da URL.

// front/js/rsvp.js
const codigo = new URLSearchParams(location.search).get('codigo');
const API = 'http://localhost:3000';
⚠️Pega-ratão

⚠️ Pega-ratão importante: essa página NÃO usa o helper api() do módulo 05 (seção 5.6). O helper foi desenhado pra área logada: ele anexa o token e, no 401, limpa o storage e redireciona pro login. O Caio não tem login. Se a página pública usar o helper, um erro qualquer pode jogar o convidado numa tela de login que não é pra ele. Aqui é fetch direto, e esse porquê é ótimo pro Módulo D: saber quando não usar a própria ferramenta é maturidade de projeto.

O carregamento com os quatro estados do módulo 05 (seção 5.5), mais um quinto ("respondido"):

async function carregarConvite() {
    mostrarEstado('carregando');
    try {
        const resposta = await fetch(`${API}/convites/${codigo}`);
        if (resposta.status === 404) return mostrarEstado('nao-encontrado');
        if (!resposta.ok) throw new Error();
        const convite = await resposta.json();
        preencherTela(convite);
        // convite ja respondido? mostra o estado final, sem formulario
        mostrarEstado(convite.status === 'pendente' ? 'formulario' : 'ja-respondido');
    } catch {
        mostrarEstado('erro'); // servidor fora, sem internet...
    }
}

O envio da resposta, com os hábitos do módulo 05 (botão desabilitado durante o request, mensagens por status):

async function enviarResposta(valor) {
    const botoes = document.querySelectorAll('.acoes button');
    botoes.forEach(b => (b.disabled = true));
    try {
        const resposta = await fetch(`${API}/convites/${codigo}/resposta`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                resposta: valor,
                acompanhantes: Number(document.querySelector('#acompanhantes').value)
            })
        });
        const dados = await resposta.json();
        if (resposta.status === 409) return avisar(dados.erro, 'alerta');
        if (!resposta.ok) return avisar('Não deu pra registrar. Tenta de novo.', 'erro');
        mostrarEstado('respondido'); // tela de sucesso, com o resumo da resposta
    } catch {
        avisar('Sem conexão com o servidor.', 'erro');
    } finally {
        botoes.forEach(b => (b.disabled = false));
    }
}

Repara que o 409 chega com a mensagem específica do back ("Capacidade do evento esgotada", "Convite já respondido", "Prazo de resposta encerrado") e a página só exibe. Mensagem de regra de negócio nasce no back, que é onde a regra mora.

E os acompanhantes entram por um <select> de 0 a 5, não por campo livre:

<select id="acompanhantes">
    <option value="0" selected>Sem acompanhantes</option>
    <option value="1">1 acompanhante</option>
    <option value="2">2 acompanhantes</option>
    <option value="3">3 acompanhantes</option>
    <option value="4">4 acompanhantes</option>
    <option value="5">5 acompanhantes</option>
</select>

É a validação por design do módulo 06: o campo que só aceita valores válidos elimina a mensagem de erro. (O back valida mesmo assim, seção 3.3, porque request não nasce só de tela.)

3.5 Enviar o convite: e-mail de verdade e a gambiarra oficial

O POST /convites é da Olívia (matriz da seção 2.1). O fluxo dele: conferir se o convidado já tem convite (a UNIQUE de convidado_id garante no banco; reenvio reaproveita o mesmo código e só atualiza enviado_em), gerar o código com retry (seção 3.2), gravar com prazo_resposta padrão de 7 dias antes do evento (DATE_SUB(data_evento, INTERVAL 7 DAY)), e entregar o link pro convidado.

Entregar como? Duas respostas, e as duas precisam estar no bolso.

O envio de e-mail de verdade. Em Node, o padrão de mercado é o nodemailer:

const nodemailer = require('nodemailer');

const transporte = nodemailer.createTransport({
    host: 'smtp.ethereal.email', // no treino: Ethereal/Mailtrap; na prova: o que a infra der
    port: 587,
    auth: { user: 'usuario-de-teste', pass: 'senha-de-teste' }
});

async function enviarConvitePorEmail(convidado, codigo) {
    await transporte.sendMail({
        from: '"Wedding Pass" <convites@weddingpass.com>',
        to: convidado.email,
        subject: 'Você foi convidado! Confirme sua presença',
        html: `<p>Oi, ${convidado.nome}!</p>
               <p><a href="http://localhost:5500/rsvp.html?codigo=${codigo}">
                  Clique aqui pra responder ao convite</a></p>`
    });
}

Em PHP, o equivalente é o PHPMailer (mesma estrutura: configura SMTP, monta destinatário/assunto/corpo, send()). Existe o mail() nativo, mas ele depende de servidor de e-mail configurado na máquina e falha em silêncio quando não tem: em ambiente de prova, é armadilha. Se for PHP, PHPMailer com SMTP de teste.

No treino, Ethereal (Node, gera conta fake na hora e mostra o e-mail num painel web) e Mailtrap (caixa de entrada de teste, serve pras duas stacks) deixam vocês verem o e-mail chegando sem mandar nada pra ninguém de verdade.

🛠️Gambiarra boa

🛠️ Gambiarra boa (e oficial): o botão "copiar link". E-mail depende de infra que a prova pode não ter (SMTP liberado, internet). O fluxo à prova de ambiente: o botão "Enviar convite" gera o código, grava enviado_em, e abre um modal com o link rsvp.html?codigo=X e um botão que copia (navigator.clipboard.writeText(link)) pra Olívia colar no WhatsApp do convidado. O fluxo de convite fica completo e demonstrável sem depender de nada externo.

A regra de decisão: se o enunciado diz "enviar por e-mail", implementa o envio (e mantém o copiar-link como plano B visível). Se diz só "convite com link", o copiar-link é a solução, não um atalho.

async function copiarLink(codigo) {
    const link = `${location.origin}/rsvp.html?codigo=${codigo}`;
    await navigator.clipboard.writeText(link);
    avisar('Link copiado! Cola no WhatsApp do convidado.', 'sucesso');
}

3.6 Follow-up: a lista de trabalho da Olívia

Depois dos convites na rua, a pergunta da Olívia é "quem falta?". Follow-up é isso: consultas por status bem apresentadas.

O panorama (alimenta o dashboard da seção 6 também):

-- LEFT JOIN pra contar tambem convidado que ainda nem tem convite
SELECT COALESCE(c.status, 'sem convite') AS situacao,
       COUNT(*) AS total
  FROM convidado cv
  LEFT JOIN convite c ON c.convidado_id = cv.id
 WHERE cv.casamento_id = ?
 GROUP BY situacao;

A lista de pendentes com prazo, que é a tela de ação:

SELECT cv.nome, c.codigo, c.enviado_em, c.prazo_resposta,
       DATEDIFF(c.prazo_resposta, CURDATE()) AS dias_restantes
  FROM convite c
  JOIN convidado cv ON cv.id = c.convidado_id
 WHERE cv.casamento_id = ?
   AND c.status = 'pendente'
 ORDER BY c.prazo_resposta;

DATEDIFF devolve a diferença em dias: positivo é prazo correndo, negativo é prazo estourado. Essa conta vem pronta do banco; o front não fica fazendo matemática de data (fuso e formato de data em JS são um campo minado que não vale a pena atravessar na prova).

A tela: filtros por status (botõezinhos "Todos / Pendentes / Confirmados / Recusados" refazendo o render, padrão do módulo 05 seção 9), badge de dias restantes com a cor semântica dos tokens do módulo 06 (verde folgado, amarelo com 3 dias ou menos, vermelho estourado), e em cada linha os botões "copiar link" e "reenviar". Nada de tela nova do zero: é o render() de lista que vocês já dominam, com dados novos.


4. Capacidade por mesa 🆕

A tabela mesa existe desde o módulo 03 (com capacidade e a UNIQUE de casamento_id + numero), mas até agora era só modelo. Agora vira feature: a Olívia distribui os convidados nas mesas, e nenhuma mesa pode passar da capacidade dela.

4.1 A consulta de ocupação

SELECT m.id, m.numero, m.capacidade,
       COALESCE(SUM(1 + cv.acompanhantes), 0) AS ocupados
  FROM mesa m
  LEFT JOIN convidado cv ON cv.mesa_id = m.id
 WHERE m.casamento_id = ?
 GROUP BY m.id, m.numero, m.capacidade
 ORDER BY m.numero;

Cada convidado ocupa 1 + acompanhantes lugares. O LEFT JOIN + COALESCE garante que mesa vazia aparece com ocupados = 0 em vez de sumir do resultado (clássico do módulo 03).

Uma decisão de projeto escondida na régua: conta quem? Se a régua for só quem confirmou, a variante entra com CASE:

-- variante: so confirmados ocupam lugar (exige juntar o convite)
COALESCE(SUM(CASE WHEN c.status = 'confirmado'
                  THEN 1 + cv.acompanhantes END), 0) AS ocupados

Recomendação: todo convidado alocado reserva o lugar, confirmado ou não. Defesa: a mesa é planejamento físico; se a Olívia sentou o convidado na mesa 3, o lugar tá comprometido até ela tirar. Contar só confirmados faz a mesa "caber" no sistema e estourar na vida real se todo mundo responder sim. As duas réguas são defensáveis; o que não pode é não saber qual você escolheu. (Se o enunciado definir, o enunciado manda.)

4.2 A regra no back: terceira aparição do molde

Alocar convidado em mesa é um PUT/PATCH no convidado com mesa_id. E aí, de novo, dois requests simultâneos podem disputar o último lugar da mesa. A esta altura vocês já sabem o molde: travar a linha-pai, contar, decidir.

// dentro de transacao, no PUT /convidados/:id quando vem mesa_id
const [mesas] = await conexao.execute(
    `SELECT m.numero, m.capacidade,
            COALESCE((SELECT SUM(1 + cv.acompanhantes)
                        FROM convidado cv
                       WHERE cv.mesa_id = m.id
                         AND cv.id <> ?), 0) AS ocupados
       FROM mesa m
      WHERE m.id = ?
      FOR UPDATE`,
    [convidadoId, mesaId] // cv.id <> ? : nao conta o proprio convidado trocando de mesa
);
const mesa = mesas[0];
if (Number(mesa.ocupados) + 1 + acompanhantes > mesa.capacidade) {
    await conexao.rollback();
    return res.status(409).json({
        erro: `Mesa ${mesa.numero} cheia (${mesa.ocupados}/${mesa.capacidade})`
    });
}
// cabe: segue o UPDATE do convidado e commita

Contando as aparições do molde: capacidade do evento no RSVP (travou casamento), capacidade da mesa aqui (travou mesa). A terceira "capacidade" do sistema, o check-in, resolve diferente de propósito, e a seção 5 explica por quê. Saber os dois jeitos e quando usar cada um é conversa de nota 3 no Módulo D.

4.3 A tela de mesas

Dois pedaços de front, os dois reaproveitando o módulo 05:

O select de alocação conta a ocupação pra Olívia na hora da escolha, e desabilita mesa cheia (validação por design de novo):

select.innerHTML = mesas.map(m => {
    const livres = m.capacidade - m.ocupados;
    return `<option value="${m.id}" ${livres <= 0 ? 'disabled' : ''}>
        Mesa ${m.numero} — ${livres} de ${m.capacidade} livres
    </option>`;
}).join('');

O painel de mesas: um grid repeat(auto-fit, minmax(220px, 1fr)) (módulo 05, seção 3.6) com um card por mesa e uma barrinha de ocupação (uma div com width em porcentagem, cor semântica virando âmbar quando encosta na lotação). É visual, é rápido de fazer com os tokens prontos, e é o tipo de tela que segura o olhar da banca na demo.


5. Check-in por código, blindado nas três camadas 🆕

No dia do casamento, a Renata na porta. Fila andando. O fluxo dela: o convidado mostra o código (impresso, no celular), ela digita ou escaneia, o sistema mostra quem é pra conferência (nome, mesa, acompanhantes, situação do convite), ela confirma a entrada. Verde grande de "bem-vindo". E se o código entrar de novo: bloqueio, com horário da primeira entrada.

A Regional tinha check-in; a versão da Seletiva é por código e blindada de ponta a ponta. Esse é o assunto que mais concentra pontos por linha de código do sistema inteiro.

5.1 As duas portas da mesma sala

O módulo 04 (seção 2.4) já tinha o POST /convidados/:id/checkin (check-in pelo id, usado na listagem de convidados). Entra agora o POST /checkins com corpo { "codigo": "A7XK2MQP" }: a porta rápida da Renata. Duas rotas, a mesma tabela checkin, a mesma constraint por trás. Nenhuma regra duplicada: as duas portas dão na mesma sala.

5.2 As três defesas

Defesa 1, o banco (a verdade final). A constraint do módulo 03:

CONSTRAINT uq_checkin_convidado UNIQUE (convidado_id)

Aconteça o que acontecer nas outras camadas, o banco não aceita duas linhas de check-in pro mesmo convidado. Essa é a garantia que não depende de ninguém se comportar.

Defesa 2, o back: INSERT direto e captura do erro. O jeito ingênuo é "SELECT pra ver se já tem, depois INSERT". Tem uma janela de corrida aí: dois requests fazem o SELECT juntos, ninguém acha nada, os dois INSERTam... e o segundo estoura na constraint com um erro 500 feio. O jeito robusto abraça a constraint: manda o INSERT direto e trata a duplicidade como resposta esperada.

Node:

router.post('/checkins', autenticar, permitir('recepcao', 'admin'), async (req, res, next) => {
    try {
        const { codigo } = req.body;

        // 1. Achar o convidado pelo codigo (e trazer o contexto pra tela de conferencia)
        const [convidados] = await pool.execute(
            `SELECT cv.id, cv.nome, cv.acompanhantes, m.numero AS mesa, c.status
               FROM convite c
               JOIN convidado cv ON cv.id = c.convidado_id
               LEFT JOIN mesa m ON m.id = cv.mesa_id
              WHERE c.codigo = ?`,
            [codigo]
        );
        if (convidados.length === 0) {
            return res.status(404).json({ erro: 'Código não encontrado' });
        }
        const convidado = convidados[0];

        // 2. INSERT direto: o banco decide quem chegou primeiro
        try {
            await pool.execute(
                'INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)',
                [convidado.id, req.usuario.id]
            );
        } catch (erro) {
            if (erro.code === 'ER_DUP_ENTRY') {
                const [[registro]] = await pool.execute(
                    'SELECT registrado_em FROM checkin WHERE convidado_id = ?',
                    [convidado.id]
                );
                const hora = new Date(registro.registrado_em)
                    .toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
                return res.status(409).json({
                    erro: 'Convidado já fez check-in',
                    detalhes: [`Entrou às ${hora}`]
                });
            }
            throw erro; // qualquer outro erro segue pro handler padrao
        }

        res.status(201).json({ mensagem: `Bem-vindo, ${convidado.nome}!`, convidado });
    } catch (erro) { next(erro); }
});

PHP (o miolo da captura):

try {
    $pdo->prepare('INSERT INTO checkin (convidado_id, registrado_por) VALUES (?, ?)')
        ->execute([$convidadoId, $usuario['id']]);
} catch (PDOException $e) {
    if ($e->errorInfo[1] == 1062) { // 1062 = codigo MySQL de entrada duplicada
        $busca = $pdo->prepare('SELECT registrado_em FROM checkin WHERE convidado_id = ?');
        $busca->execute([$convidadoId]);
        $registro = $busca->fetch();
        responder(409, [
            'erro' => 'Convidado já fez check-in',
            'detalhes' => ['Entrou às ' . date('H:i', strtotime($registro['registrado_em']))]
        ]);
    }
    throw $e;
}

Repara numa coisa: sem transação. É um INSERT só, e a constraint é o árbitro: se dois requests chegarem colados, o banco aceita um e rejeita o outro, atomicamente. Compara com os dois usos do FOR UPDATE (RSVP e mesa): lá a decisão precisava de uma conta antes da escrita, então travava-se pra contar. Aqui a regra é "só pode existir um", que é exatamente o que uma UNIQUE declara. Duas ferramentas pra mesma família de problema: quando a regra é contável, transação com trava; quando a regra é unicidade, constraint e captura. Essa frase, dita com as próprias palavras no Módulo D, é diferença entre 2 e 3.

Defesa 3, o front. O painel da Renata (módulo 05, seções 6 e 7.3): resposta 201 pinta o painel verde com nome e mesa; 409 pinta vermelho com a mensagem e o horário que veio em detalhes. O campo de código com autofocus e limpo após cada operação, porque a persona da Renata (módulo 06) opera com fila na frente: cada segundo de fricção é fila parada.

5.3 Provar a corrida em 30 segundos

A banca testa dupla entrada abrindo duas abas e mandando o mesmo código quase junto. Vocês vão provar isso antes, direto no console do navegador:

const opts = {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
    body: JSON.stringify({ codigo: 'A7XK2MQP' })
};
const respostas = await Promise.all([
    fetch('http://localhost:3000/checkins', opts),
    fetch('http://localhost:3000/checkins', opts)
]);
console.log(respostas.map(r => r.status));
// esperado: [201, 409] (em qualquer ordem). NUNCA [201, 201].

Dois requests disparados no mesmo tick. Se voltar [201, 409], a blindagem tá de pé. Se voltar [201, 201], tem duas linhas no banco e um problema pra resolver agora (provavelmente a constraint não existe: SHOW CREATE TABLE checkin pra conferir). Esse teste entra no hábito, junto com a bateria do token fraco.

5.4 E se o convite não estiver confirmado?

O convidado chega na porta com convite pendente (nunca respondeu) ou recusado (disse que não ia e apareceu). Entra? Três respostas possíveis: barrar, permitir direto, ou permitir com aviso (painel amarelo: "Convite não confirmado. Registrar entrada mesmo assim?", e a Renata decide na hora com um clique).

Recomendação: permitir com aviso. Defesa: na porta de um casamento de verdade, ninguém barra a tia que esqueceu de responder; o sistema informa e a pessoa decide. É a melhor história pro Módulo D porque mostra que vocês pensaram no contexto de uso, não só na regra. Como sempre: se o enunciado definir o comportamento, o enunciado manda.

🛠️Gambiarra boa

🛠️ Gambiarra boa: o QR code. O código impresso no convite pode virar QR. Custo mínimo, efeito grande na demo. Dois caminhos: a lib qrcode do npm, local (QRCode.toDataURL(link) devolve a imagem pronta pra pôr num <img>), ou o serviço https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=... numa linha de HTML (depende de internet: confirmar se a prova tem antes de contar com isso). Ler QR pela câmera (API BarcodeDetector, uns 15 linhas) só se sobrar tempo de verdade: é cereja, não bolo. No Módulo D, o QR é o tipo de "a mais" que mostra visão de produto real.

E o registrado_por gravado em cada check-in fecha a história: é auditoria ("quem deixou essa pessoa entrar, e quando?"), e é mais um porquê pronto pro banco do Módulo D: é por causa dele que check-in é da recepção logada, e não um endpoint aberto.


6. Dashboard: agregações reais, gráfico e alerta de 90% 🆕

A tela da Olívia. Ela abre e enxerga o evento de relance: quantos convidados, quantos confirmaram, quantos já chegaram, e o alerta quando a ocupação passa de 90% da capacidade.

6.1 Um endpoint, uma viagem

A tentação é o front fazer cinco fetches (convidados, confirmados, presentes, mesas...). O desenho certo é um endpoint que devolve o dashboard inteiro: GET /casamentos/:id/dashboard. Uma viagem ao servidor, uma tela montada. Os números nascem de duas ou três consultas de agregação (módulo 03), e a técnica central é o SUM(CASE WHEN ...), que conta várias coisas numa passada só:

SELECT COUNT(*) AS convidados,
       SUM(CASE WHEN c.status = 'confirmado' THEN 1 ELSE 0 END) AS confirmados,
       SUM(CASE WHEN c.status = 'recusado'  THEN 1 ELSE 0 END) AS recusados,
       SUM(CASE WHEN c.status = 'pendente' OR c.status IS NULL THEN 1 ELSE 0 END) AS pendentes,
       COALESCE(SUM(CASE WHEN c.status = 'confirmado'
                         THEN 1 + cv.acompanhantes END), 0) AS pessoas_esperadas
  FROM convidado cv
  LEFT JOIN convite c ON c.convidado_id = cv.id
 WHERE cv.casamento_id = ?;

(pendentes inclui o IS NULL: convidado sem convite ainda também "falta responder" do ponto de vista da Olívia. E pessoas_esperadas conta acompanhantes, porque lugar sentado não sabe a diferença.)

Mais uma pros presentes:

SELECT COUNT(*) AS presentes
  FROM checkin ch
  JOIN convidado cv ON cv.id = ch.convidado_id
 WHERE cv.casamento_id = ?;

E o endpoint monta a resposta, com a regra do alerta calculada aqui:

const ocupacaoPct = Math.round((pessoas_esperadas / capacidade_total) * 100);

res.json({
    convidados, confirmados, recusados, pendentes,
    pessoas_esperadas,
    presentes,
    capacidade_total,
    ocupacao_pct: ocupacaoPct,
    alerta_capacidade: ocupacaoPct >= 90,
    mesas // o array da consulta de ocupacao da secao 4.1
});
⚠️Pega-ratão

⚠️ Pega-ratão: calcular o alerta no front. Se o >= 90 morar no JS da tela, a banca abre o endpoint no Postman e vê um JSON sem a regra: e ela desconta onde a regra deveria morar (back, módulo 04 seção 11.1). O front recebe alerta_capacidade: true e só decide a cor. Regra no back, cosmética no front, sempre.

⚠️ E o pega-ratão clássico continua valendo: chumbar números bonitos na tela pra "ficar apresentável". A banca faz um check-in ao vivo e olha se o contador de presentes subiu. Se não reagiu, o "tempo real" era mentira e cai nota. Todo número do dashboard nasce de consulta.

6.2 Gráfico sem biblioteca (o caminho que sempre funciona)

Enunciado pode permitir biblioteca, pode não permitir, e a internet pode não existir na hora. O gráfico que sempre pode: HTML + CSS puros.

Barras (confirmados × recusados × pendentes), ~15 linhas:

<div class="grafico">
    <div class="barra" style="--v: 62%"><span>Confirmados</span></div>
    <div class="barra recusados" style="--v: 10%"><span>Recusados</span></div>
    <div class="barra pendentes" style="--v: 28%"><span>Pendentes</span></div>
</div>
.grafico { display: flex; align-items: flex-end; gap: var(--espaco-2); height: 160px; }
.barra {
    flex: 1;
    height: var(--v);                 /* o JS seta --v com o percentual real */
    background: var(--cor-sucesso);
    border-radius: var(--raio) var(--raio) 0 0;
    transition: height 300ms ease;    /* o grafico "cresce" no refresh */
}
.barra.recusados  { background: var(--cor-erro); }
.barra.pendentes  { background: var(--cor-alerta); }

O JS só faz barra.style.setProperty('--v', pct + '%'). Com os tokens do módulo 06 já definidos, isso é bonito por padrão.

Donut de ocupação, 3 linhas de CSS com conic-gradient:

.donut {
    width: 140px; aspect-ratio: 1; border-radius: 50%;
    background: conic-gradient(var(--cor-acao) 0 var(--pct), #e5e7eb var(--pct) 100%);
    /* furo do donut: um circulo interno com a cor do fundo, ou mask */
}

Com biblioteca, se o enunciado permitir e valer o tempo: Chart.js, arquivo local no projeto (front/lib/chart.umd.js, baixado antes). CDN como única via é aposta na internet da prova, e essa aposta já custou demo de muita gente. A regra: o caminho sem lib no músculo, a lib como upgrade.

6.3 "Tempo real" que é real

Duas pontas: o dashboard recarrega sozinho (setInterval de 15 segundos chamando o fetch de novo, módulo 05 seção 7.2) e recarrega após ação própria (fez check-in pelo painel? chama a atualização na sequência). O alerta de 90% é uma classe CSS trocada conforme alerta_capacidade, com a cor de status dos tokens. Quando a banca fizer o check-in ao vivo dela, o contador sobe em até 15 segundos sem ninguém apertar F5: esse momento, sozinho, conta a história de sistema vivo.


7. Comprovante em PDF e JPEG 🆕

Saída de documento: o comprovante da confirmação (ou do check-in) que o convidado recebe ou guarda. O enunciado da Seletiva pede PDF e JPEG. Três caminhos pra ter no bolso, em ordem de investimento:

7.1 O caminho 1 (o primeiro a fazer): uma página bem formatada + impressão do navegador

Monta comprovante.html?codigo=X: uma página caprichada com os tokens do módulo 06 (nome do casal, nome do convidado, código grande, QR se tiver, mesa, status). Aí:

PDF de graça, com CSS de impressão:

@media print {
    .nao-imprime { display: none; }   /* botoes, menu: some na impressao */
    body { background: white; }
}
@page { size: A5; margin: 12mm; }
document.querySelector('#btn-pdf').addEventListener('click', () => window.print());
// o dialogo do navegador oferece "Salvar como PDF": documento pronto, tamanho A5

JPEG com html2canvas (biblioteca de um arquivo só, baixada local):

const area = document.querySelector('#comprovante');
const canvas = await html2canvas(area);          // fotografa o elemento
const link = document.createElement('a');
link.href = canvas.toDataURL('image/jpeg', 0.92); // qualidade 92%
link.download = `comprovante-${codigo}.jpg`;
link.click();                                     // dispara o download
🛠️Gambiarra boa

🛠️ Por que esse caminho primeiro: UMA página cobre os DOIS formatos, e a mesma página ainda aparece na demo. O esforço vai todo pro que pontua (o capricho visual), e a "geração de documento" custa um window.print() e dez linhas de JS. É exatamente o atalho honesto que dev de produto usa.

7.2 O caminho 2 (Node): PDF gerado no servidor com pdfkit

Se o enunciado pedir download gerado pelo servidor (ou pontos de back nessa feature):

const PDFDocument = require('pdfkit');

router.get('/convites/:codigo/comprovante.pdf', async (req, res, next) => {
    try {
        // ...busca convite + convidado + casamento; 404 se nao existir...
        const doc = new PDFDocument({ size: 'A5', margin: 40 });
        res.setHeader('Content-Type', 'application/pdf');
        res.setHeader('Content-Disposition', 'attachment; filename=comprovante.pdf');
        doc.pipe(res); // o PDF escorre direto pra resposta HTTP

        doc.fontSize(20).text('Wedding Pass', { align: 'center' });
        doc.moveDown();
        doc.fontSize(14).text(`Convidado: ${convidado.nome}`);
        doc.text(`Código: ${convite.codigo}`);
        doc.text(`Mesa: ${convidado.mesa ?? 'a definir'}`);
        doc.text(`Status: ${convite.status}`);

        doc.end();
    } catch (erro) { next(erro); }
});

7.3 O caminho 3 (PHP): dompdf, reaproveitando o HTML

O dompdf converte HTML em PDF, então o mesmo HTML do comprovante da seção 7.1 serve de template:

require 'vendor/autoload.php';
use Dompdf\Dompdf;

$dompdf = new Dompdf();
$dompdf->loadHtml($htmlDoComprovante); // o mesmo HTML da pagina, com CSS embutido
$dompdf->setPaper('A5');
$dompdf->render();
$dompdf->stream('comprovante.pdf');    // dispara o download

A decisão, resumida: caminho 1 sempre (cobre PDF + JPEG com uma página e serve pra demo); caminhos 2/3 quando o enunciado cobrar geração no servidor. E em qualquer caminho, o investimento certo é na página bonita primeiro, porque ela alimenta os três.

⚠️Pega-ratão

⚠️ Pega-ratão: comprovante feio (dados desalinhados, sem identidade visual). A banca abre o arquivo. Um comprovante com os tokens do evento, tipografia decente e o QR alinhado é acabamento nível 3; um <pre> com os dados jogados é nível 1 com a mesma lógica por trás. Mesma regra do módulo 06: o que o usuário final recebe conta história sobre o sistema inteiro.


8. Branch e merge no ritmo de features 🔄

Essas duas semanas são o cenário perfeito pra rodar o fluxo do módulo 02 de verdade: uma branch por feature.

git switch -c feat/rsvp        # nasce a branch da feature
# ...commits pequenos enquanto constroi: banco, endpoint, tela...
git switch main
git merge feat/rsvp            # feature pronta entra na main

Commits pequenos e frequentes, mensagens no padrão que vocês já usam (feat:, fix:, chore:, sem acentos). No treino, cada feature deste módulo nasce numa branch e entra na main por merge, inclusive com um conflito ensaiado de propósito perto do fim (o guia de aula marca o dia): conflito resolvido com calma no treino é conflito que não assusta na prova.

E na prova? Regra prática: branch só se estiver no músculo, sem custo mental. O inegociável é o resto: commits frequentes na main já são backup contra desastre e evidência de processo pra banca. Branch bem usada é ponto extra de organização; branch mal usada (código preso fora da main na hora da entrega) é risco. Conforto decide.


9. Como o CIS enxerga as funcionalidades novas 🎯

Cada feature deste módulo espalha pontos por mais de um critério. O mapa:

Funcionalidade Onde pontua Como a banca testa
RSVP público Back (regra de capacidade, validação) + front (4 estados da página) Código inválido na URL, responder duas vezes, confirmar com o evento quase cheio
Três perfis Back (403 rota a rota) + UX (menu e tela por perfil) Token fraco em rota forte, direto na API
Check-in por código Back (corrida bloqueada) + front (painel com feedback) + UX (fluxo da fila) Duas abas com o mesmo código quase juntas; espera o 409 com o horário
Dashboard Banco (agregações) + back (alerta no lugar certo) + front (hierarquia visual) Check-in ao vivo olhando se o contador sobe sozinho
Mesas Banco (modelo + consulta de ocupação) + back (regra de lotação) Alocar convidado em mesa cheia
Comprovante Front + UX (acabamento nível 3) Abre o PDF e o JPEG e olha o capricho

Lendo a coluna da direita de uma vez, dá pra ver quem é a banca: um usuário apressado e malicioso ao mesmo tempo. Ela usa o sistema como a Renata numa fila (rápido, sem ler manual) e como alguém tentando quebrar (URL trocada, ação repetida, token errado). Todo teste de vocês de agora em diante imita esses dois humores.


10. O ritual da Prova Surpresa (Módulo A) 🆕

O Módulo A da Seletiva entrega um requisito desconhecido e 2 horas. É a compressão máxima do que este módulo treina. O ritual, com o relógio na mesa:

Relógio O que tá acontecendo
0:00 – 0:10 Ler o enunciado grifando: must vs should (módulo 09 se vier em inglês)
0:10 – 0:20 O método da seção 1 no papel: entidades, endpoints, telas. O papel é o plano das 2h
0:20 – 0:50 Banco (ALTER/CREATE no migrations.sql) + endpoint com a regra de negócio + prova no Postman
0:50 – 1:40 A tela, integrada de verdade, com os 4 estados
1:40 – 2:00 Testes de sabotador (os dois humores da banca) + commits finais + respiro

A ordem de corte quando o tempo aperta (e ele vai apertar): primeiro cai o enfeite visual extra, depois o gráfico, depois o segundo formato de saída (JPEG). O que nunca cai: a regra de negócio no back e a validação. Uma feature crua com a regra certa pontua; uma feature linda que aceita dado inválido conta a história errada pra banca inteira.

E commit a cada peça que funciona (banco pronto: commit; endpoint provado no Postman: commit; tela integrada: commit). Se algo quebrar feio aos 1:50, o último commit é a rede.

🎯Foco de prova

🎯 Esse ritual se treina. O guia de aula fecha as duas semanas rodando ele completo, e os treinos avaliativos repetem. Na Seletiva, o ritual tem que estar tão automático quanto o SELECT.


11. Como estudar esse módulo

Esse módulo não se estuda decorando, se estuda construindo. Pra cada funcionalidade, a trilha completa tem que sair sem consulta:

  1. Dados: que entidades e campos isso exige? Que constraint protege a regra no último nível? (módulo 03)
  2. Regra: que regra de negócio a API garante, com que status ela responde, e onde entra trava ou constraint? (módulo 04 + seções 3 a 5 daqui)
  3. Tela: como o usuário faz isso, com que estados e que feedback? (módulos 05 e 06)
  4. Prova: o que aqui é objetivo (sim/não) e o que é julgamento (0 a 3)? Onde mora a nota? (seção 9)

Quem percorre essa trilha pra cada feature entendeu o sistema como um todo, e é isso que a prova mede: não pedaços soltos, um sistema inteiro que aguenta uso apressado e malicioso.


12. Autoavaliação do módulo

  1. Quais são os quatro passos do método e o que sai de cada um?
  2. Por que a matriz de rotas vale mais que a "escada" de permissões? Qual exceção do Wedding Pass a escada erraria, e qual a defesa dela?
  3. Descreve o roteiro do teste do token fraco no Postman, do login ao status esperado.
  4. Desenha o ciclo de estados do convite. Onde cada estado mora no banco, e por que "presente" não é um valor do ENUM?
  5. Por que o código do convite não pode ser o id sequencial? E como o sistema lida com colisão de códigos?
  6. O que o FOR UPDATE serializa no RSVP? Conta a história dos dois "sim" simultâneos no último lugar.
  7. Por que a página pública do RSVP não usa o helper api() do módulo 05?
  8. Quando o botão "copiar link" resolve o requisito do convite, e quando precisa de envio de e-mail de verdade?
  9. Escreve de cabeça a consulta dos pendentes com dias restantes (DATEDIFF).
  10. Quais são as três defesas do check-in? Por que o INSERT-e-captura dispensa transação nesse caso?
  11. Como provar em 30 segundos que a dupla entrada tá bloqueada?
  12. Por que o dashboard é UM endpoint, e por que o alerta de 90% tem que nascer no back?
  13. Como fazer o gráfico de barras e o donut sem biblioteca nenhuma? E qual a condição pra usar Chart.js?
  14. Quais os três caminhos do comprovante e por que a página bem formatada vem primeiro?
  15. Na capacidade da mesa: alocados ou só confirmados? Tua escolha e tua defesa.
  16. Recita o timebox do Módulo A e a ordem de corte quando o tempo aperta.

Referências do módulo

Do plano de curso (UC1):

Documentação oficial (o "mercado" deste módulo):


💡Nota

Sistema completo na cabeça? Agora vem o mês que transforma "funciona" em "impecável": 08 — Lapidação e qualidade de código.

Competições Senac RS · Seletiva 26

Módulo 08

Módulo 08 — Lapidação e qualidade de código

BaseUC4 do plano de curso (Desenvolver código orientado a objetos, 92h)Peso na provanão tem peso próprio, é o acabamento que empurra todas as notas de julgamento de 2 pra 3NaturezaRefino
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. O cronograma reserva outubro pra isso. Nesse ponto o sistema já roda ponta a ponta: banco modelado (03), API com autenticação (04), front que consome a API (05), interface pensada (06) e as funcionalidades novas montadas (07). Lapidação é o mês que transforma "funciona" em "impecável". É o menos glamouroso e o mais decisivo do treino inteiro: na prática é aqui que se ganha a competição, porque a diferença entre 2 e 3 (o nosso jogo inteiro, lembra do módulo 01) mora quase toda no acabamento. Não é conteúdo novo, é a demão final em cima de tudo que já existe.

Deixando claro pra quem não é técnico: isso é 🆕 novo no sentido de que a etapa Regional não chega aqui. Quem lapida está jogando o jogo do nível 3. As duas já sabem construir o sistema; esse módulo é sobre deixar o sistema redondo, e sobre fazer isso rápido, porque na prova o tempo de lapidar é curto.

🔗 O fio que atravessa 07 e 08: compressão. No módulo 07 a gente treinou montar uma feature inteira em quatro horas. Aqui é o irmão dele: lapidar um sistema inteiro nas horas finais da prova. Os dois vivem do mesmo segredo, ter os padrões prontos na mão. Máscara você não inventa na prova, você cola a que já treinou. Componente de confirmação você não desenha na hora, você reusa. Esse módulo é a caixa de ferramentas que a gente monta em outubro pra sacar em dezembro sem pensar.


1. O que é lapidação (e o que não é)

Lapidar não é adicionar funcionalidade. É pegar o que já funciona e deixar redondo: sem aresta, sem susto, sem feiúra. É a distância entre um sistema que passa no teste feliz (o "caminho certo", que a própria competidora testa porque sabe usar o que fez) e um sistema que aguenta o mundo real: a usuária fazendo besteira, o dado faltando, a internet caindo, o clique duplo, o campo vazio.

Uma forma de enxergar: todo sistema tem dois públicos. O primeiro é a pessoa que usa direito. O segundo é a pessoa que usa errado, de propósito ou por acidente. O caminho feliz atende o primeiro. A lapidação atende o segundo. E a banca é, por profissão, o segundo público.

🎯Foco de prova

🎯 Por que isso é o coração do nível 3. Quase toda avaliação de julgamento (0 a 3) pergunta "está bem-feito?", e "bem-feito" é acabamento. Duas competidoras entregam o mesmo CRUD de convidado. Uma valida o formulário, dá mensagem clara, tem máscara no telefone, confirma antes de apagar e mostra "nenhum convidado ainda" na lista vazia. A outra não. Mesma funcionalidade, notas diferentes em UX, em qualidade de código e em robustez. Três critérios subindo de uma vez por causa de acabamento. Lapidação é onde essa diferença é construída de propósito, não por sorte.

⚠️ Pega-ratão de calendário. Lapidação não é uma etapa que acontece "no fim". As máscaras, os nomes bons, o componente de confirmação, tudo isso a gente combina e treina desde agosto, senão outubro não dá conta. O que acontece em outubro é a passada final: revisar tudo com o checklist na mão. Se a base já veio limpa, a passada é rápida. Se veio suja, outubro vira faxina e não sobra tempo pra brilhar. Padronizar no começo custa barato; padronizar no fim custa a prova.

1.1 O mapa da lapidação

Pra não virar "melhorar no olho", a lapidação tem endereço. Toda a passada final cabe em seis frentes, e é assim que esse módulo está organizado:

Frente O que é Onde toca
Entrada máscara e formatação enquanto digita (§2) front
Validação regex e casos de canto (§3, §4) front + back + banco
Segurança de ação confirmar antes de destruir (§5) front
Navegação de dados busca, filtro, ordenação (§6) banco → back → front
Código por dentro refatoração, OO, código limpo, padrão (§7, §8) tudo
Comunicação de erro mensagem que diz o que fazer (§9) tudo

E fechando, a mentalidade que garante que nada disso passou batido: testar como a banca testa (§10). Cada frente vira item de checklist. É esse checklist que a gente saca na prova.


2. Máscaras e formatação de entrada

Campo que se formata sozinho enquanto a pessoa digita: telefone que vira (51) 99999-9999, data que vira 31/08/2026, valor que vira R$ 1.250,00. Parece detalhe, mas comunica cuidado já na primeira olhada, e previne erro na origem (a usuária não consegue digitar letra num campo de telefone). É das coisas de melhor retorno pelo esforço na prova inteira.

Só que máscara tem uma regra de ouro que separa quem sabe de quem só copiou: formata pra mostrar, guarda limpo pra mandar.

2.1 O padrão formata-mostra / guarda-limpo

O banco não quer (51) 99999-9999, quer 51999999999. O DECIMAL do módulo 03 não quer R$ 1.250,00, quer 1250.00. Então a máscara vive só na tela. Na hora de mandar pra API (o api() do módulo 05), a gente limpa o valor de volta pro cru.

São sempre duas funções por máscara: uma que formata (cru → bonito, roda a cada tecla) e uma que limpa (bonito → cru, roda antes de enviar).

// mascaras.js — front, vale igual nas duas stacks

// TELEFONE: formata enquanto digita, limpa antes de mandar
function formataTelefone(valor) {
  const numeros = valor.replace(/\D/g, '').slice(0, 11); // só dígitos, no máx 11
  if (numeros.length <= 2)  return numeros;
  if (numeros.length <= 7)  return `(${numeros.slice(0,2)}) ${numeros.slice(2)}`;
  return `(${numeros.slice(0,2)}) ${numeros.slice(2,7)}-${numeros.slice(7)}`;
}

function limpaTelefone(valor) {
  return valor.replace(/\D/g, ''); // guarda só os números
}

Ligar no campo é um addEventListener de input (dispara a cada tecla). O truque de sempre reposicionar o cursor no fim resolve 99% dos casos da prova sem complicar:

const campoTel = document.querySelector('#telefone');
campoTel.addEventListener('input', (e) => {
  e.target.value = formataTelefone(e.target.value);
});

E na hora de enviar, limpa:

const payload = {
  nome: campoNome.value.trim(),
  telefone: limpaTelefone(campoTel.value), // "51999999999", não "(51) 99999-9999"
};
await api('/convidados', { method: 'POST', body: payload });
🛠️Gambiarra boa

🛠️ Gambiarra boa: uma máscara genérica por template. Em vez de escrever uma função por campo, dá pra ter uma que segue um molde de # (dígito) e caractere fixo. Cola pronta na prova e serve pra telefone, CEP, data, o que aparecer:

js function mascara(valor, molde) { // molde ex.: "(##) #####-####" ou "##/##/####" const nums = valor.replace(/\D/g, ''); let i = 0, saida = ''; for (const ch of molde) { if (i >= nums.length) break; saida += (ch === '#') ? nums[i++] : ch; } return saida; } // uso: mascara(v, "(##) #####-####") ou mascara(v, "##/##/####")

Uma função na cola, todas as máscaras da prova resolvidas. Isso é compressão: você não pensa em máscara na hora, você aplica o molde.

2.2 Dinheiro é o caso chato (e cai)

Valor em reais tem vírgula decimal, ponto de milhar e o R$ na frente. E o back quer 1250.00 (ponto decimal, sem milhar). O caminho que menos dá dor de cabeça na prova é formatar na saída do campo (evento blur) e ter o par de conversão sempre à mão:

// mostra: 1250.5 -> "R$ 1.250,50"
function formataReais(numero) {
  return numero.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
}
// limpa: "R$ 1.250,50" -> 1250.5 (número que o DECIMAL aceita)
function limpaReais(texto) {
  const cru = texto.replace(/[^\d,]/g, '').replace(',', '.');
  return parseFloat(cru) || 0;
}
⚠️Pega-ratão

⚠️ Pega-ratão clássico: mandar "R$ 1.250,00" pro campo DECIMAL(10,2) do banco. O MySQL não entende, ou pior, entende errado e guarda 1.00. Todo campo de dinheiro precisa passar pelo limpaReais antes de virar payload. Formata pra mostrar, guarda limpo: é a mesma regra do telefone, e é a que a banca verifica olhando o que chegou no banco, não o que aparece na tela.

🔗 Continuidade. No módulo 05 a máscara apareceu como parte da validação no cliente, meio de passagem. Aqui ela vira padrão fechado, com o par formata/limpa explícito. Guarda esse par: ele é o que transforma "tem máscara" (bonito) em "tem máscara certa" (bonito e o dado chega limpo no banco). A banca de nível 3 testa os dois.


3. Biblioteca de regex pra validação

Regex (expressão regular) é o canivete da validação: um padrão que diz "esse texto tem a cara certa?". Vale a pena chegar na prova com uma bibliotequinha pronta na cola, porque validar formato é ponto garantido e escrever regex sob pressão é onde todo mundo trava.

// validacao.js — os padrões que caem no Wedding Pass
const REGEX = {
  email:    /^[^\s@]+@[^\s@]+\.[^\s@]+$/,        // algo@algo.algo
  telefone: /^\d{10,11}$/,                        // 10 ou 11 dígitos (já limpo)
  data:     /^\d{2}\/\d{2}\/\d{4}$/,              // dd/mm/aaaa
  codigo:   /^[A-Z0-9]{8}$/,                      // código do convite, CHAR(8)
  soLetras: /^[A-Za-zÀ-ÿ\s]+$/,                   // nome sem número
};

function pareceEmail(v)    { return REGEX.email.test(v.trim()); }
function pareceTelefone(v) { return REGEX.telefone.test(v.replace(/\D/g, '')); }

3.1 CPF: regex não basta, precisa do dígito verificador

Um erro comum é achar que /^\d{11}$/ valida CPF. Valida o formato (onze dígitos), não o CPF. 00000000000 passa nesse regex e não é CPF nenhum. CPF de verdade tem dígito verificador, e a banca sabe disso. A função abaixo é feia mas é a que separa nível 2 de nível 3 num campo de documento:

function cpfValido(cpf) {
  cpf = cpf.replace(/\D/g, '');
  if (cpf.length !== 11 || /^(\d)\1{10}$/.test(cpf)) return false; // 111.111... é inválido

  const digito = (fatiaAte) => {
    let soma = 0, peso = fatiaAte + 1;
    for (let i = 0; i < fatiaAte; i++) soma += Number(cpf[i]) * (peso--);
    const resto = (soma * 10) % 11;
    return resto === 10 ? 0 : resto;
  };
  return digito(9) === Number(cpf[9]) && digito(10) === Number(cpf[10]);
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: o Wedding Pass talvez nem peça CPF (o tema é casamento, não banco). Mas leva a função na cola mesmo assim: se a Prova Surpresa (módulo 07) jogar um campo de documento, você cola e ganha o ponto de validação forte enquanto os outros só contam onze dígitos. Preparo é o que vira sorte na prova.

⚠️ Pega-ratão de regex de e-mail. Tem gente que decora um regex de e-mail gigante de trinta caracteres achando que é mais "certo". Na prova, /^[^\s@]+@[^\s@]+\.[^\s@]+$/ (tem arroba, tem ponto depois, sem espaço) resolve tudo que a banca testa e você lembra de cabeça. Regex perfeito de e-mail não existe e não paga. O suficiente e memorizável ganha do perfeito e esquecido.

No PHP a lógica é idêntica, muda a função: preg_match('/^[^\s@]+@[^\s@]+\.[^\s@]+$/', $email) no lugar do .test(). O padrão entre barras é o mesmo texto. Regex é uma linguagem só, viaja entre as stacks sem tradução.


4. Validações finas: os cantos que ninguém testa

O caminho feliz qualquer uma faz. Lapidação é cuidar dos cantos, e o jeito de não esquecer nenhum é varrer campo por campo perguntando "e se vier vazio? e se vier errado? e se vier absurdo?". Vira uma tabelinha por tela:

Campo Vazio Errado Absurdo
nome "informe o nome" número no nome 300 caracteres
telefone opcional, ok menos de 10 dígitos letra
capacidade da mesa "informe a capacidade" texto negativa, 0, ou 5000
data do evento "informe a data" formato inválido data no passado
acompanhantes assume 0 texto 500 num casamento

Cada linha dessas tratada com uma resposta clara é um ponto que sobe. Cada uma que quebra a tela é um ponto que cai.

💡Nota

🔗 As três camadas (do módulo 04, agora fechadas). Validação boa mora em três lugares e cada um tem um papel: - Front valida pra ser gentil: avisa na hora, antes de gastar uma ida ao servidor ("a data precisa ser futura"). - Back valida pra ser seguro: nunca confia no que chegou, porque o front pode ser burlado (Postman, DevTools). É o "front esconde, back barra" do módulo 04. - Banco valida pra ser íntegro: o CHECK (capacidade > 0) e o NOT NULL do módulo 03 são a última muralha, valem mesmo se todo o resto falhar.

A banca adora furar a camada de front (manda direto pela API) pra ver se o back segura. Validar só no front é nível 2 com cara de nível 3: bonito até alguém abrir o DevTools.

🎯 A banca testa o caminho que falha. Quando você desenvolve, testa o que dá certo, porque sabe usar o próprio sistema. A banca não sabe e nem quer: vai clicar errado, deixar em branco, mandar lixo, cadastrar o mesmo convidado duas vezes. Cada caso desses previsto e respondido é robustez, que pontua. A pergunta que abre esse ponto ("e se vier vazio, errado, absurdo?") é literalmente o roteiro que a banca segue. Antecipa ela.


5. Componente de confirmação de ação destrutiva

Toda ação que apaga ou desfaz algo importante pede uma pergunta antes: "tem certeza que quer apagar essa convidada?". É prevenção de erro (Nielsen, módulo 06) na veia, e evita o desastre do clique acidental que apaga a lista toda na frente da banca.

5.1 A gambiarra que já serve

O confirm() nativo do navegador resolve na marra e não custa nada:

if (confirm('Apagar essa convidada? Essa ação não volta.')) {
  await api(`/convidados/${id}`, { method: 'DELETE' });
  render();
}

Funciona, protege, e num aperto de tempo está de bom tamanho. Mas é feio (aquela caixa cinza do navegador quebra a identidade visual que a gente cuidou no módulo 06) e trava a página inteira enquanto está aberto.

5.2 O componente reusável (o que dá nível 3)

O pulo do gato é ter um componente de confirmação que combina com o sistema e serve pra tudo: apagar convidada, cancelar convite, resetar o evento. Faz uma vez, usa em todo lugar (componentização, módulo 05). A sacada é ele devolver uma Promise, pra usar com await igualzinho ao confirm() nativo, sem espalhar callback pelo código:

// confirmar.js — um componente, toda confirmação do sistema
function confirmar(mensagem) {
  return new Promise((resolve) => {
    const fundo = document.createElement('div');
    fundo.className = 'modal-fundo';
    fundo.innerHTML = `
      <div class="modal-caixa" role="alertdialog" aria-modal="true">
        <p>${mensagem}</p>
        <div class="modal-acoes">
          <button class="btn-cancelar" autofocus>Cancelar</button>
          <button class="btn-confirmar">Confirmar</button>
        </div>
      </div>`;
    document.body.appendChild(fundo);

    const fecha = (resposta) => { fundo.remove(); resolve(resposta); };
    fundo.querySelector('.btn-confirmar').onclick = () => fecha(true);
    fundo.querySelector('.btn-cancelar').onclick  = () => fecha(false);
    fundo.onkeydown = (e) => { if (e.key === 'Escape') fecha(false); }; // Esc cancela
  });
}

Uso no lugar do nativo, mesma cara, visual do sistema:

if (await confirmar('Apagar essa convidada? Essa ação não volta.')) {
  await api(`/convidados/${id}`, { method: 'DELETE' });
  render();
}
🛠️Gambiarra boa

🛠️ Gambiarra boa: o botão perigoso (Confirmar de um DELETE) na cor de erro (--cor-erro, o token do módulo 05) e o Cancelar com autofocus. Aí um Enter distraído cancela em vez de apagar. É segurança de graça: o caminho fácil é o caminho seguro. Nielsen aprovaria.

⚠️ Pega-ratão de acessibilidade que a banca de UX cobra. Modal precisa de três coisas pra não perder ponto: fechar no Esc, foco começar dentro dele (o autofocus no Cancelar) e o role="alertdialog". São três detalhes que somam num critério que pesa 10% (módulo 06). Barato de pôr, caro de esquecer.

🔗 Continuidade. Nos módulos anteriores a gente usou confirm() cru sem culpa, pra não travar o aprendizado. Aqui ele "cresce" pro componente honesto. Esse é o movimento da lapidação inteira: pega o rascunho que funcionava e troca pela versão redonda. Rascunho na construção, componente na entrega.


6. Busca, filtro e ordenação de ponta a ponta

Quando as listas crescem (e o seed em massa do módulo 03 faz elas crescerem de propósito), rolar a tela atrás de uma convidada é sofrimento. Lapidação adiciona três coisas que andam juntas: busca (achar pelo nome ou código), filtro (ver só confirmados, só pendentes, só quem já entrou) e ordenação (por nome, por status, por hora do check-in).

O ponto que pontua: isso acontece no servidor, não no front. A API recebe o que filtrar e o banco devolve só o que interessa, com o WHERE e o ORDER BY dos módulos 03 e 04. Trazer a lista inteira e filtrar no navegador funciona com 10 convidados e derrete com 500. Mostrar que você sabe onde cada coisa mora (o banco filtra, o back orquestra, o front mostra) é a maturidade que a banca lê como nível 3, e é como escala no mercado.

6.1 O SQL é um só

-- busca por nome OU código, filtro por status, ordenação escolhida
SELECT c.id, c.nome, c.status, ck.registrado_em
FROM convidado c
LEFT JOIN checkin ck ON ck.convidado_id = c.id
WHERE (c.nome LIKE ? OR c.codigo LIKE ?)
  AND (? = 'todos' OR c.status = ?)
ORDER BY c.nome ASC;

O LIKE com %termo% acha pedaço de nome. O truque do (? = 'todos' OR c.status = ?) deixa o mesmo SQL servir pra "todos" e pra um status específico, sem montar query diferente pra cada caso.

6.2 O endpoint muda de sintaxe entre as stacks

Conceito idêntico: ler os parâmetros da URL (?busca=ana&status=confirmado&ordem=nome), montar a consulta com prepared statement (nunca concatenar string, é a porta da SQL injection do módulo 04) e devolver o resultado.

Node + Express + mysql2:

app.get('/convidados', async (req, res) => {
  const { busca = '', status = 'todos', ordem = 'nome' } = req.query;

  // ordenação não entra por prepared statement (é nome de coluna), então valida por lista branca
  const colunas = { nome: 'c.nome', status: 'c.status', checkin: 'ck.registrado_em' };
  const orderBy = colunas[ordem] || 'c.nome';

  const termo = `%${busca}%`;
  const [linhas] = await db.query(
    `SELECT c.id, c.nome, c.status, ck.registrado_em
       FROM convidado c
       LEFT JOIN checkin ck ON ck.convidado_id = c.id
      WHERE (c.nome LIKE ? OR c.codigo LIKE ?)
        AND (? = 'todos' OR c.status = ?)
      ORDER BY ${orderBy} ASC`,
    [termo, termo, status, status]
  );
  res.json(linhas);
});

PHP + PDO:

$busca  = $_GET['busca']  ?? '';
$status = $_GET['status'] ?? 'todos';
$ordem  = $_GET['ordem']  ?? 'nome';

$colunas = ['nome' => 'c.nome', 'status' => 'c.status', 'checkin' => 'ck.registrado_em'];
$orderBy = $colunas[$ordem] ?? 'c.nome'; // lista branca, mesmo motivo

$termo = "%{$busca}%";
$sql = "SELECT c.id, c.nome, c.status, ck.registrado_em
          FROM convidado c
          LEFT JOIN checkin ck ON ck.convidado_id = c.id
         WHERE (c.nome LIKE ? OR c.codigo LIKE ?)
           AND (? = 'todos' OR c.status = ?)
         ORDER BY {$orderBy} ASC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$termo, $termo, $status, $status]);
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
⚠️Pega-ratão

⚠️ Pega-ratão que é armadilha de segurança. O ORDER BY não aceita prepared statement, porque nome de coluna não é valor. A tentação é concatenar ORDER BY ${req.query.ordem} direto, e aí abriu SQL injection pela porta dos fundos. A defesa é a lista branca: um objeto que mapeia o que a usuária pediu pro nome real da coluna, e cai no padrão se pedir algo fora da lista. Isso a banca de back testa mandando ?ordem=; DROP TABLE.

6.3 No front, debounce pra não sufocar a API

Busca que dispara uma requisição a cada tecla manda dez chamadas pra escrever "roberto". O debounce segura: só busca quando a pessoa para de digitar por um tempinho.

function debounce(fn, ms = 300) {
  let t;
  return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}

const buscar = debounce(async (termo) => {
  const lista = await api(`/convidados?busca=${encodeURIComponent(termo)}`);
  renderLista(lista); // reusa o render dos quatro estados do módulo 05
}, 300);

campoBusca.addEventListener('input', (e) => buscar(e.target.value));
🛠️Gambiarra boa

🛠️ Gambiarra boa: o debounce é uma função de quatro linhas que você cola e usa em busca, em auto-save, em qualquer coisa que dispara com o teclado. Mais um item pra caixa de ferramentas da compressão: pequeno, genérico, sempre útil.

🔗 Continuidade. A renderLista aqui é a mesma dos quatro estados do módulo 05 (carregando, vazio, erro, sucesso). Busca que não acha nada cai no estado vazio com "nenhuma convidada encontrada", não numa tela branca. Lapidação é onde os estados vazios finalmente cobrem todos os cantos, inclusive o resultado de busca sem resultado.


7. Código limpo, refatoração e OO (UC4)

Aqui a lapidação vira pra dentro do código. O sistema funciona, mas o código pode estar embolado, e a banca lê o código (é critério de julgamento explícito). Refatorar é melhorar a organização sem mudar o comportamento. Código limpo não é frescura: é o que faz a banca dar nível 3 em qualidade e é o que faz você achar o bug rápido quando o relógio da prova aperta. As duas coisas ao mesmo tempo.

7.1 Os quatro movimentos que pagam

Não precisa decorar os SOLID inteiros. Na prova, quatro movimentos resolvem quase tudo (é o Clean Code do Martin filtrado pro que a banca vê):

1. Nome que explica. Variável e função com nome que diz o que é.

// antes: ninguém sabe o que é
const x = c.filter(i => i.s === 'confirmado');
// depois: o nome é a documentação
const convidadosConfirmados = convidados.filter(c => c.status === 'confirmado');

2. Função curta que faz uma coisa. Se a função tem "e" no meio do que ela faz (valida e salva e renderiza), são três funções.

// antes: uma função que faz tudo, 40 linhas
async function salvar() { /* valida, monta payload, chama api, trata erro, re-renderiza... */ }
// depois: cada passo com nome, o salvar vira um roteiro legível
async function salvar() {
  const erros = validarFormulario();
  if (erros.length) return mostrarErros(erros);
  const payload = montarPayload();
  await enviarConvidado(payload);
  await recarregarLista();
}

3. DRY, o mesmo código num lugar só. O trecho copiado em cinco telas vira uma função. Se precisar mudar, muda num lugar (menos bug). O api() do módulo 05 já é isso: em vez de fetch repetido com header de token em toda tela, um helper só.

4. Sem número mágico, sem código morto. O 90 do alerta de capacidade (módulo 07) vira const LIMITE_ALERTA = 0.9. A função comentada que ninguém usa mais sai (o Git guarda a história, módulo 02, então apagar é seguro). Código morto confunde a banca e confunde você.

7.2 Um toque de orientação a objetos (UC4)

A UC4 é sobre código orientado a objetos, e dá pra pontuar com pouco: agrupar o que anda junto. Em vez de funções soltas de validação espalhadas, uma peça com responsabilidade clara. Pode ser uma classe ou um módulo, o que a stack pedir:

// validadorConvidado.js — encapsula as regras de uma convidada num lugar só
class ValidadorConvidado {
  constructor(dados) { this.dados = dados; this.erros = []; }

  validar() {
    if (!this.dados.nome?.trim())        this.erros.push('O nome é obrigatório.');
    if (this.dados.email && !pareceEmail(this.dados.email))
      this.erros.push('E-mail inválido.');
    if (this.dados.acompanhantes > 5)    this.erros.push('Máximo de 5 acompanhantes.');
    return this.erros;
  }
}
// uso: const erros = new ValidadorConvidado(payload).validar();

Ganhou três coisas: as regras da convidada moram num lugar (encapsulamento), o resto do código não precisa saber como valida (só chama .validar()), e pra mudar uma regra você sabe exatamente onde ir. Isso é OO servindo a lapidação, não OO por enfeite.

🛠️Gambiarra boa

🛠️ Gambiarra boa: o padrão de fábrica pra mensagem de erro. Se aparece muito { erro: '...', detalhes: [...] } (o formato do módulo 04), uma funçãozinha erroDeValidacao(detalhes) que monta o objeto padroniza a resposta inteira do back. É o embrião do padrão Factory do Gang of Four, na dose que a prova aproveita: uma função que fabrica o objeto sempre igual. Padrão de projeto na competição é isso, a versão de bolso que economiza linha e mantém consistência, não o diagrama de vinte classes.

7.3 Refatorar com segurança: passo pequeno, teste, repete

Refatoração tem uma regra de sobrevivência: um passo de cada vez, testando entre um e outro. Renomeou uma função? Roda, vê se ainda funciona, commita (módulo 02). Só então extrai a próxima. Assim, se quebrou, você sabe exatamente o que quebrou (foi a última coisa) e o git te devolve pra antes num comando.

⚠️Pega-ratão

⚠️ Pega-ratão que já custou competição. Refatorar tudo de uma vez na véspera da entrega, sem testar, e quebrar o que funcionava. Refatoração é atividade de quando sobra tempo e serve pra melhorar com segurança, testando a cada passo e commitando. Nunca a última coisa antes de entregar, com pressa e sem rede. Se faltam 20 minutos e está funcionando, a refatoração fica pra vida; entrega o que roda.

🎯 Como a banca lê isso. Organização e legibilidade são critério explícito de julgamento. Abrir o arquivo e ver nomes que explicam, funções curtas e nenhum trecho repetido cinco vezes é o que separa o 2 do 3 em "qualidade de código". E tem o bônus escondido: código limpo é código onde você acha o próprio bug rápido. Na prova, isso é tempo, e tempo é mais ponto.


8. Padronização: o sistema parecendo feito por uma pessoa só

São duas competidoras codando o mesmo sistema. Sem combinação, sai uma colcha de retalhos: uma escreve nomeConvidado, a outra guest_name; uma indenta com dois espaços, a outra com quatro; um botão de ação é roxo numa tela e azul na outra. A banca percebe na hora, e isso derruba a nota de consistência (a heurística de consistência do Nielsen, módulo 06, agora aplicada ao código inteiro).

Consistência de ponta a ponta é combinar antes:

🛠️Gambiarra boa

🛠️ Gambiarra boa: deixa a máquina padronizar. Um formatador automático (Prettier no Node, ou o formatador do próprio VS Code) alinha indentação e aspas com um atalho, sem discussão. E um arquivo .editorconfig na raiz faz as duas máquinas usarem a mesma configuração. Combina isso na primeira semana de treino, não na última: aí o código já nasce padronizado e a lapidação de outubro nem precisa olhar pra isso.

⚠️ Pega-ratão de calendário (de novo, porque é o que mais mata). Padronizar no fim é retrabalho caro: renomear variável em quinze arquivos na véspera é pedir bug. Padronizar no começo é de graça, porque já sai certo. Essa é a decisão mais barata do treino inteiro e a que mais rende em outubro. Combina o padrão em agosto e cumpre.


9. Mensagens de erro: a passada que mais paga

O módulo 06 já bateu nisso e a lapidação fecha de vez: passar o sistema inteiro trocando todo "deu erro" genérico por uma mensagem que diz o que aconteceu e o que fazer. É o ganho de nível 3 mais barato que existe, porque não muda lógica nenhuma, só troca texto, e sobe a percepção de qualidade da tela toda.

Genérico (nível 2) Útil (nível 3)
"Erro ao salvar" "O nome da convidada é obrigatório."
"Falha" "Esse código de convite não existe."
"Não permitido" "Essa convidada já fez check-in às 19h32."
"Erro 500" "Não deu pra salvar agora. Tenta de novo em instantes."
"Campo inválido" "A data do evento precisa ser futura."

O segredo pra isso ser rápido é o back já mandar a mensagem pronta no formato único do módulo 04 ({ erro: 'texto pra pessoa', detalhes: [...] }) e o front só exibir o que chegou (o avisar() do módulo 05), em vez de cada tela inventar seu próprio texto. Mensagem boa nasce no back, viaja no formato padrão, aparece no componente padrão. Uma passada final só pra revisar as mensagens é das coisas de melhor retorno na reta final.

🎯Foco de prova

🎯 Onde a banca sente. Ela vai errar de propósito só pra ler a mensagem. "Esse código de convite não existe" diz que o sistema entende o que houve. "Erro" diz que o sistema não faz ideia. Mesma falha por baixo, percepção de qualidade oposta. É o critério de julgamento mais fácil de subir com o menor esforço, e o que mais gente esquece porque está ocupada com o caminho feliz.


10. Testar como a banca vai testar (UC11)

O plano de curso tem uma UC inteira de testes (UC11), e ela não pede teste automatizado sofisticado na prova. Pede a mentalidade de teste: antes de dizer "pronto", usar o sistema tentando quebrar. Testar é procurar defeito de propósito, não confirmar que funciona.

10.1 O vocabulário que a banca (e a prova escrita) usa

Vale saber os nomes, porque a Prova Objetiva (módulo 01) cobra e falar a língua sobe a percepção na apresentação (módulo 10):

Termo O que é No Wedding Pass
Caso de teste uma entrada + o resultado esperado "convidada sem nome → mensagem 'nome obrigatório'"
Plano de teste a lista de casos que vou rodar o checklist do sabotador (abaixo)
Caixa-preta testar pela interface, sem olhar o código clicar na tela como a banca
Caixa-branca testar sabendo o código por dentro forçar o catch do check-in duplo
Teste de fronteira os limites de um valor capacidade 0, 1, e a máxima; 5 e 6 acompanhantes
Teste de regressão reconferir o que já funcionava depois de mexer apagar convidada e ver se a lista não quebrou
Smoke test a passada rápida "acende sem soltar fumaça?" login → cadastra → confirma → check-in, ponta a ponta
🎯Foco de prova

🎯 Teste de fronteira é ouro barato. A banca ama o limite: se a mesa é CHECK BETWEEN 1 AND 50, ela testa 0, testa 50, testa 51. Se acompanhantes vai até 5, ela testa 5 e 6. Quem validou a fronteira certa (o "até 5" aceita 5 e barra 6) ganha; quem errou o <= pro < perde. Testar os próprios limites antes é achar esse bug antes da banca achar.

10.2 O checklist do sabotador (o plano de teste da prova)

Esse é o instrumento central da UC11 na prática. Na reta final, uma das duas usa o sistema de propósito errado enquanto a outra anota o que quebra. É simular a banca antes da banca. Cada linha que quebrou vira item pra lapidar:

# Sabotagem Deveria acontecer Quebrou?
1 Enviar formulário todo vazio mensagens de campo obrigatório, nada some
2 Clicar "Salvar" duas vezes rápido salva uma vez só (botão desabilita, módulo 05)
3 Digitar letra no telefone máscara não deixa
4 Fazer check-in da mesma convidada duas vezes "já fez check-in às HH:MM" (corrida, módulo 04/07)
5 Apagar sem confirmar pede confirmação (§5)
6 Abrir uma tela interna sem logar manda pro login (guarda de rota, módulo 05)
7 Login com senha errada "e-mail ou senha inválidos", sem vazar qual
8 Recepção tentando abrir tela de admin 403, escondido no front e barrado no back (módulo 07)
9 Apertar a janela pra celular layout responsivo aguenta (módulo 05/06)
10 Buscar um nome que não existe estado vazio "nenhuma encontrada", não tela branca
11 Confirmar RSVP depois do prazo recusa educada (módulo 07)
12 Cadastrar convidada além da capacidade barra com aviso (regra do módulo 07)
🛠️Gambiarra boa

🛠️ Gambiarra boa: essa tabela vira um .md no repositório e a dupla passa nela antes de qualquer entrega parcial (os TAs) e antes da final. É o plano de teste da UC11 sem nenhuma ferramenta, só disciplina. Testar o próprio sistema com má vontade é o melhor treino de nível 3 que existe, e é reusável: a mesma tabela roda em agosto, em outubro e em dezembro.

🔗 Continuidade. No começo do treino o checklist do sabotador era um parágrafo solto. Aqui ele fecha como o instrumento da UC11: um plano de teste de verdade, com coluna de "quebrou?", que conversa com cada módulo anterior (a corrida do 04, a guarda do 05, os perfis do 07). Ele é o resumo executável do treino inteiro: se tudo passa nessa tabela, o sistema está no nível 3.


11. Como o CIS enxerga a lapidação

Lapidação não tem uma linha própria na planilha CIS. Ela é transversal: sobe as notas de julgamento espalhadas por vários critérios ao mesmo tempo. É por isso que rende tanto. Onde cada frente desse módulo aparece na hora da correção:

O que a banca vê na tela/código Frente deste módulo Critério que sobe
campo com máscara, dado limpo no banco §2 Front + Banco
formulário que barra entrada inválida nas três camadas §3, §4 Front + Back
confirmação antes de apagar, modal acessível §5 UX
busca/filtro/ordenação no servidor §6 Back + Banco
código com nomes claros, funções curtas, sem repetição §7 Config/Qualidade
visual e código consistentes de ponta a ponta §8 UX + Qualidade
mensagem de erro que diz o que fazer §9 UX + Front
sistema que aguenta o uso errado §10 Robustez (todos)
🎯Foco de prova

🎯 A conta que fecha a competição. Repara que a coluna da direita repete Front, UX, Back, Qualidade. É o mesmo acabamento marcando ponto em critérios diferentes de uma vez. Uma máscara bem-feita conta em Front e em Banco. Uma mensagem de erro boa conta em UX e em Front. É por isso que o módulo 01 diz que a diferença entre 2 e 3 é o jogo inteiro: a lapidação é o multiplicador que pega uma hora de trabalho e espalha em pontos por toda a planilha. Nenhuma feature nova rende assim.


12. Autoavaliação do módulo

Responder em voz alta, como se explicasse pra banca. Onde travar, o número da seção diz pra onde voltar.

  1. Qual a diferença entre lapidar e adicionar funcionalidade? Por que a lapidação decide a competição mesmo sem ter peso próprio? (§1, §11)
  2. Explica o padrão "formata pra mostrar, guarda limpo". Por que mandar "(51) 99999-9999" pro banco é erro? (§2)
  3. Escreve de cabeça a máscara genérica por molde de #. Pra que campos ela serve? (§2.1)
  4. Por que /^\d{11}$/ não valida um CPF? O que falta? (§3.1)
  5. Por que um regex de e-mail simples ganha do "perfeito" na prova? (§3)
  6. Preenche a tabela "vazio / errado / absurdo" pro campo capacidade da mesa. (§4)
  7. Onde moram as três camadas de validação e qual o papel de cada uma? Por que validar só no front é armadilha? (§4)
  8. Por que o componente de confirmação devolve uma Promise? Cita os três detalhes de acessibilidade que ele precisa ter. (§5)
  9. Por que busca e filtro devem acontecer no servidor e não no front? (§6)
  10. O que é a lista branca de colunas no ORDER BY e qual ataque ela previne? (§6.2)
  11. O que é debounce e que problema ele resolve na busca? (§6.3)
  12. Cita os quatro movimentos de código limpo que mais pagam na prova, com um exemplo de cada. (§7.1)
  13. Como um pouco de OO (uma classe validadora) ajuda a lapidação? (§7.2)
  14. Qual a regra de ouro pra refatorar sem quebrar? Por que nunca refatorar na véspera sem testar? (§7.3)
  15. Reescreve três mensagens de erro genéricas pra mensagens úteis do Wedding Pass. (§9)
  16. Diferencia caso de teste, teste de fronteira e teste de regressão com um exemplo do Wedding Pass em cada. (§10.1)
  17. Como funciona o checklist do sabotador e por que ele é o plano de teste da UC11? (§10.2)
  18. Por que uma máscara bem-feita rende ponto em dois critérios do CIS ao mesmo tempo? (§11)

Referências do módulo

Do plano de curso (bibliografia das UCs):

Complementares de mercado (código limpo e refino):

💡Nota

Sistema lapidado? Falta a competência que corre por baixo do treino inteiro e vale 15% da prova: 09 — Inglês técnico. Depois dela, o módulo que transforma tudo isso em nota na frente da banca: 10 — Apresentação.

Competições Senac RS · Seletiva 26

Módulo 09

Módulo 09 — Inglês técnico

BaseUC5 do plano de curso (Analisar orientações técnicas em inglês escrito, 108h)Peso na prova15%NaturezaRevisão contínua
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. O sistema tá lapidado por dentro e por fora (módulo 08). Os dois últimos módulos da apostila não entregam tela nenhuma e mesmo assim decidem pódio: esse aqui e a apresentação. Inglês vale 15% da prova. É o terceiro maior peso, atrás só de front (40%) e back+banco (20%): mais que UX, mais que banco de dados, o triplo de versionamento. E as duas UCs que sustentam o módulo são as maiores do plano de curso inteiro, 108 horas cada, 216 somadas. O Senac não deu esse tamanho à toa. Só que inglês não tem semana no cronograma: tem o treino inteiro. Por isso esse módulo se lê diferente dos outros. O glossário (§5) e o catálogo de drills (§8) entram em uso já na primeira semana de agosto, porque alimentam o aquecimento diário de toda aula. O resto acompanha o treino e se consolida nas duas aulas dedicadas do guia.


1. Como 15% de inglês aparecem numa prova de programação

Não existe "a prova de inglês" com hora marcada. O inglês da Seletiva é instrumental: ele atravessa os quatro módulos do Projeto-Teste e pontua onde a competidora nem sempre percebe que está sendo avaliada.

🎯Foco de prova

🎯 Onde mora o ponto. Os 15% de peso são a parte declarada. A parte escondida é que o inglês alavanca os outros 85%: enunciado bem lido vira requisito certo (front, back, banco), erro bem lido vira tempo recuperado, doc bem lida vira feature destravada. Inglês ruim não perde só os 15. Ele cobra pedágio em tudo.

⚠️ Pega-ratão: subestimar porque "não é tela". É a competência mais fácil de deixar pra depois, justamente porque não tem uma entrega visível. O antídoto é o formato desse módulo: nunca é uma tarde inteira de inglês, é 10 minutos todos os dias, sem exceção, de agosto até a véspera da prova.


2. A gramática que cai: inglês instrumental de requisito

Esquece conjugação de verbo irregular. O inglês que decide ponto na Seletiva cabe em meia dúzia de estruturas que aparecem em todo enunciado e em toda doc. Dominar essas estruturas é gramática a serviço da leitura de requisito.

2.1 Os modais que definem o que é obrigatório

Especificação técnica em inglês usa os modais com sentido padronizado. Existe até um documento clássico que fixou essa convenção pra internet inteira, a RFC 2119, e as provas seguem a mesma lógica:

Palavra no enunciado Força O que significa na prática
must / shall / required Obrigatório Se não tiver, perde ponto. Prioridade máxima.
must not / shall not Proibido Se tiver, perde ponto. Tão grave quanto faltar um must.
should / recommended Recomendado A banca espera. Só fica de fora se o tempo acabar, e por decisão consciente.
should not Desaconselhado Evitar, a menos que haja um bom motivo pra defender.
may / can / optional Opcional Diferencial. Faz depois que os musts e shoulds estão de pé.

Um trecho de enunciado do universo Wedding Pass, do jeito que poderia cair:

💡Nota

"The system must prevent duplicate check-ins. Each guest may bring up to five companions. The organizer should receive an alert when a table reaches 90% of its capacity."

Leitura de competidora nota 3: check-in duplo bloqueado é inegociável (must, e é a constraint UNIQUE do módulo 03 mais a verificação atômica do módulo 07); acompanhantes são permitidos com limite de cinco, incluindo o cinco (may + up to); o alerta de 90% é esperado, entra no plano, mas depois dos musts (should).

🎯Foco de prova

🎯 Modal lido vira ordem de ataque. A técnica de grifar requisitos do módulo 01 depende disso: must é a primeira lista, should é a segunda, may é a terceira. Quem trata tudo como igual gasta tempo de must em coisa de may, e o CIS não perdoa must faltando.

2.2 O imperativo das instruções

Documentação e orientação técnica falam com verbo no imperativo, sem sujeito: "Create a database named wedding_pass.", "Run the following command.", "Add the dependency to your project.". Imperativo em doc não é grosseria, é instrução: faça isso, nessa ordem. Quando uma orientação vem numerada em imperativo, a sequência importa; pular passo é a receita do erro esquisito dez minutos depois.

2.3 Condicionais de comportamento: if X, then Y

Todo comportamento de sistema se descreve com condicional, e o enunciado usa isso o tempo todo:

A diferença fina que muda implementação: if descreve uma possibilidade, when descreve uma regra que roda sempre que o evento acontece. Unless engana porque inverte: a frase acima diz que o check-in só passa com convite confirmado. Quem lê unless como "a não ser" solto costuma implementar a condição ao contrário.

2.4 Quantidades e limites

Requisito com número é requisito com pegadinha de fronteira:

Expressão Significado Cuidado
at least 8 characters no mínimo 8 8 passa
at most / no more than 5 no máximo 5 5 passa, 6 não
up to five companions até cinco inclui o cinco
more than 100 guests mais de 100 100 não conta
between 1 and 10 entre 1 e 10 em spec, normalmente inclusivo nas pontas
per table / each guest por mesa / cada convidado define o escopo da regra (a regra roda por linha, não no total)
exceeds the capacity ultrapassa a capacidade igual à capacidade ainda não excedeu
⚠️Pega-ratão

⚠️ Pega-ratão clássico: implementar > onde o enunciado pedia >=. "Reaches 90% of its capacity" dispara no 90%, não depois dele. Na dúvida, traduzir a fronteira pra um caso concreto (mesa de 10 lugares: alerta com 9 confirmados) antes de escrever o código.

2.5 Voz passiva: quem faz o quê

Doc e enunciado adoram voz passiva: "The password is hashed before being stored.", "The request is rejected if the header is missing.". A pergunta de leitura é sempre a mesma: quem faz isso? Se a frase está descrevendo o comportamento esperado do sistema, quem faz é o código da competidora (ela precisa implementar o hash). Se está descrevendo o que a ferramenta já faz sozinha ("the connection is closed automatically"), é só saber que acontece. Confundir os dois gera tanto trabalho faltando quanto trabalho dobrado.

2.6 Falsos amigos e palavras que enganam

Palavra Parece É de verdade
actually atualmente na verdade
eventually eventualmente uma hora, mais cedo ou mais tarde (certeza, não possibilidade)
currently correntemente atualmente (essa sim)
push puxar empurrar, enviar (git push manda pro remoto)
pull — puxar, trazer (git pull traz do remoto)
data data dados (date é data)
library livraria biblioteca
support dar suporte muitas vezes é "aceitar, ter a capacidade" ("MySQL supports transactions" = MySQL tem transações)
resume resumo retomar, continuar (currículo é résumé ou CV)
record recorde registro, linha de tabela
argument discussão argumento de função (o valor passado no parâmetro)
parse — interpretar, transformar texto em estrutura (não tem tradução boa; adotar o termo)
🛠️Gambiarra boa

🛠️ Gambiarra boa: o par push/pull no músculo. Quem traduz literalmente inverte o Git inteiro. A âncora que resolve: push empurra pra longe (meu código vai pro GitHub), pull puxa pra perto (o código do GitHub vem pra mim). Falar em voz alta uma vez por semana no drill já fixa.


3. Ler documentação: método, não tradução

3.1 Skimming e scanning

Duas habilidades diferentes, e a prova usa 90% uma delas:

Traduzir a página inteira não é nenhuma das duas. É o hábito que mais queima tempo de prova e o primeiro que o treino desmonta.

3.2 A anatomia das páginas de doc que a gente usa

Cada doc tem um esqueleto fixo. Conhecer o esqueleto transforma a página de parede de texto em mapa:

🛠️Gambiarra boa

🛠️ Gambiarra boa: o código é a língua universal. Bloco de exemplo se entende mesmo quando o parágrafo em volta está difícil. Em qualquer doc, ler o exemplo antes do texto: na maioria das vezes o exemplo responde e o texto vira opcional.

3.3 A rota dos 30 segundos

Padrão pra qualquer dúvida de sintaxe durante o treino e a prova:

  1. Ctrl+F na página com a palavra exata (nome da função, do parâmetro ou do erro).
  2. Exemplo primeiro. Achou um bloco de código que parece com o caso? Lê ele.
  3. Syntax/Description depois, só pra confirmar a ordem dos parâmetros.
  4. Parágrafo por último, só se ainda restou dúvida (geralmente sobre um caso especial).

Se em 2 minutos a página não respondeu, a página está errada: voltar e escolher outra, em vez de insistir traduzindo.

3.4 Exercício real de scanning (com gabarito)

Fazer cronometrado, 2 minutos por rodada, sem tradutor. É o mesmo formato do Drill 2 (§8).

Rodada 1: MDN, página do fetch(). 1. Qual é o método HTTP default quando não se passa options? 2. O fetch() retorna o quê? 3. Em qual propriedade das options vai o corpo da requisição?

Rodada 2: MySQL Manual, página de Date and Time Functions. 1. Qual specifier do DATE_FORMAT imprime o dia com dois dígitos? 2. Como formatar uma data como 30/11/2026?

Rodada 3: php.net, página de password_hash(). 1. Qual algoritmo o PASSWORD_DEFAULT usa hoje? 2. A função retorna o quê? 3. Qual função faz a verificação na hora do login?

Gabarito: R1: GET · uma Promise que resolve pra um objeto Response · body (string, via JSON.stringify). R2: %d · DATE_FORMAT(data, '%d/%m/%Y'). R3: bcrypt · a string do hash (que já embute salt e custo) · password_verify().

🎯Foco de prova

🎯 Como isso cai. Ninguém decora todo specifier de data nem toda option do fetch, e a banca sabe. O que separa nota 2 de nota 3 é a velocidade de recuperação: a competidora que acha %d/%m/%Y em 40 segundos segue no ritmo; a que trava no formato de data perde 10 minutos numa coluna de tabela.


4. Anatomia do erro: ler a mensagem que resolve

O erro em inglês é a orientação técnica escrita (UC5) que mais aparece no dia da prova. E ele sempre tem a mesma estrutura, em qualquer stack:

[TIPO DO ERRO]: [o que aconteceu] [detalhe: qual valor, qual chave, qual arquivo]
    [onde: arquivo, linha, coluna]
    [stack trace: o caminho de chamadas até chegar lá]

4.1 O vocabulário que os erros repetem

Expressão Sentido Exemplo real
cannot / could not / unable to não conseguiu Cannot read properties of undefined
failed to falhou ao Failed to fetch
missing / required faltando / obrigatório JWT must be provided
expected X, got Y esperava X, veio Y Expected 2 arguments, got 1
not found não achou 404 Not Found, command not found
already exists já existe Table 'convite' already exists
denied / refused negado / recusado Access denied, connection refused
timed out estourou o tempo Connection timed out
invalid / unexpected inválido / inesperado Unexpected token '<'
out of range fora do intervalo Out of range value for column 'acompanhantes'
deprecated descontinuado (ainda roda, mas vai parar) avisos amarelos no console
🎯Foco de prova

🎯 Detalhe que separa nível: undefined vs is not defined. x is not defined significa que a variável não existe (erro de digitação ou de escopo). Cannot read properties of undefined significa que a variável existe, mas o valor dela é undefined (o objeto não veio: fetch que falhou, linha que a query não retornou, campo com nome errado). São dois bugs diferentes com caras parecidas, e quem sabe a diferença vai direto na causa.

4.2 Stack trace no Node: de cima pra baixo, até achar o seu arquivo

TypeError: Cannot read properties of undefined (reading 'nome')
    at listarConvidados (/home/comp/wedding-pass/controllers/convidadoController.js:23:35)
    at Layer.handle [as handle_request] (/home/comp/wedding-pass/node_modules/express/lib/router/layer.js:95:5)
    at next (/home/comp/wedding-pass/node_modules/express/lib/router/route.js:149:13)

Como ler, na ordem:

  1. Primeira linha: tipo (TypeError) + o que houve (tentou ler .nome de algo que é undefined).
  2. Descer até a primeira linha que é do SEU código: convidadoController.js:23:35 (arquivo, linha 23, coluna 35). Tudo que tem node_modules no caminho é o percurso interno do Express: ignora, não é lá que se mexe.
  3. Ir na linha 23 e perguntar: o que aqui pode estar undefined? (O resultado da query? O req.body? Um campo com nome trocado?)

4.3 Erro no PHP: a linha vem no final da mensagem

Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation:
1062 Duplicate entry '12' for key 'checkin.uq_checkin_convidado'
in /var/www/wedding-pass/api/checkin.php:18
Stack trace:
#0 /var/www/wedding-pass/api/checkin.php(18): PDOStatement->execute(Array)
#1 {main}

Leitura: uma exceção de PDO não tratada, violação de integridade, código MySQL 1062 (entrada duplicada) na chave única uq_checkin_convidado, disparada no checkin.php linha 18. Esse erro específico é um velho conhecido de propósito: é a constraint UNIQUE do check-in fazendo o trabalho dela (módulo 07). O bug não é o banco reclamar; é o código não ter tratado a reclamação pra devolver um 409 educado em vez de estourar na cara do usuário. Além do Fatal error (para tudo), o PHP solta Warning e Notice/Deprecated (avisos que não param a execução, mas apontam problema).

4.4 Os erros de MySQL que mais aparecem no treino

Erro O que diz Causa típica
1045 Access denied for user acesso negado usuário ou senha errados na conexão
1049 Unknown database 'wedding_pass' banco desconhecido banco não criado ou nome errado no config
1062 Duplicate entry '...' for key '...' entrada duplicada UNIQUE violado (e-mail repetido, check-in duplo)
1064 You have an error in your SQL syntax sintaxe SQL vírgula sobrando, aspas erradas, palavra reservada
1146 Table '...convidados' doesn't exist tabela não existe nome errado (a tabela é convidado, singular)
1451 Cannot delete or update a parent row FK bloqueou o pai tentou apagar registro que tem filhos (é o RESTRICT agindo)
1452 Cannot add or update a child row FK bloqueou o filho inseriu filho apontando pra um pai que não existe
ECONNREFUSED 127.0.0.1:3306 (Node) / SQLSTATE[HY000] [2002] (PHP) conexão recusada o MySQL não está rodando, ou porta/host errados

4.5 Os erros do navegador (console do front)

4.6 O método dos 30 segundos

Pra qualquer erro, sempre na mesma ordem:

  1. Tipo: que categoria de erro é? (sintaxe, conexão, constraint, undefined)
  2. Mensagem em português mental: traduzir a linha principal pra si mesma, sem tradutor.
  3. Onde: primeira linha do stack que é do meu código (arquivo:linha).
  4. Hipótese: o que naquela linha produziria essa mensagem?
  5. Teste: confirmar a hipótese (um console.log, um SELECT, um print) antes de sair mexendo.
🛠️Gambiarra boa

🛠️ Gambiarra boa: o erro entendido é o Google interno da prova. Sem internet livre, não dá pra colar a mensagem no buscador. Mas quem domina o vocabulário da §4.1 faz a "busca" de cabeça: Duplicate entry + nome da chave já diz o quê e onde. O treino diário do Drill 3 (§8) é exatamente pra instalar esse reflexo.

⚠️ Pega-ratão: ler o erro pela metade. No Node, a linha que importa é a primeira (mais o primeiro arquivo seu); no PHP, a mensagem se estica e o arquivo:linha ficam no final. E no MySQL, o número do erro (1062, 1452) diz mais que a frase em volta. Cada stack tem seu jeito, e ler pela metade em qualquer um deles leva pra caça no lugar errado.


5. O glossário por domínio

Regra de uso: essa é a base pronta. Na primeira semana de agosto ela vira um arquivo vivo no repositório de cada competidora (docs/glossario.md), e toda sexta-feira ganha os termos novos que apareceram no treino da semana (5 minutos, no fim do drill). Glossário que não cresce é glossário decorativo.

5.1 Palavras de enunciado

Termo Sentido Exemplo em contexto
must / shall deve (obrigatório) The system must validate the input.
should deveria (recomendado) The organizer should receive an alert.
may / might pode (opcional/possível) Each guest may bring companions.
required obrigatório All fields are required.
optional opcional The phone number is optional.
allowed / not allowed permitido / proibido Duplicate entries are not allowed.
at least / at most no mínimo / no máximo At least 8 characters.
up to até (inclusive) Up to five companions.
each / every cada / todo Each table has a capacity.
unless a menos que ...unless the invitation is confirmed.
otherwise caso contrário Otherwise, an error is displayed.
provided that desde que ...provided that the code is valid.
in order to para (finalidade) In order to check in, the guest...
such as como (exemplos) Formats such as PDF or JPEG.
e.g. / i.e. por exemplo / isto é A unique code (e.g., A1B2C3D4).
default padrão The default status is "pendente".
available disponível Available seats per table.
regardless of independente de ...regardless of the user role.

5.2 Banco de dados

Termo Sentido Exemplo em contexto
database / schema banco / esquema Create a database named wedding_pass.
table / row / column tabela / linha / coluna The guest table has ten columns.
record / field registro / campo Each record represents one guest.
primary key / foreign key chave primária / estrangeira A foreign key constraint fails.
constraint restrição A UNIQUE constraint prevents duplicates.
query consulta The query returns confirmed guests only.
join junção Join the guest and table tables.
sort / order by ordenar Sorted by name in ascending order.
ascending / descending crescente / decrescente Descending order: newest first.
aggregate agregar COUNT and SUM are aggregate functions.
transaction / commit / rollback transação / confirmar / desfazer The transaction is rolled back on error.
backup / restore cópia / restauração Restore the database from the dump file.
seed carga inicial Seed the database with sample data.
nullable / not null aceita nulo / não aceita The table_id column is nullable.

5.3 Back-end e API

Termo Sentido Exemplo em contexto
request / response requisição / resposta The server sends a JSON response.
endpoint / route ponto de acesso / rota The /api/guests endpoint requires a token.
method método HTTP Use the POST method to create.
header / body / payload cabeçalho / corpo / carga Send the token in the Authorization header.
status code código de status Returns 201 when the guest is created.
authentication autenticação (quem é você) JWT handles the authentication.
authorization autorização (o que você pode) Authorization is based on user roles.
role perfil/papel Three roles: admin, organizer, reception.
token / expired token / expirado The token expires in two hours.
hash resumo criptográfico Passwords are hashed with bcrypt.
middleware função intermediária The auth middleware runs before the route.
validate / validation validar / validação Server-side validation is required.
environment variable variável de ambiente The secret is stored in an environment variable.
parse interpretar/converter The server parses the JSON body.
stateless sem estado REST APIs are stateless.
concurrency / race condition concorrência / condição de corrida An atomic update prevents the race condition.

5.4 Front-end

Termo Sentido Exemplo em contexto
layout / grid disposição / grade A two-column grid layout.
responsive / breakpoint responsivo / ponto de quebra The breakpoint is at 768px.
component componente A reusable card component.
state estado The four states: loading, empty, error, success.
event / listener evento / ouvinte Add a click event listener.
render desenhar/exibir The list is rendered from the API data.
fetch buscar (a função e o verbo) The page fetches the guest list.
promise / async / await promessa / assíncrono Await the response before rendering.
input / form / submit campo / formulário / enviar Prevent the default submit behavior.
placeholder texto de exemplo no campo A placeholder shows the expected format.
disabled / hidden desabilitado / oculto The button is disabled while loading.
mask máscara de campo A mask formats the phone number.
storage armazenamento do navegador The token is stored in localStorage.
dropdown / modal lista suspensa / janela sobreposta A confirmation modal before deleting.

5.5 Git e terminal

Termo Sentido Exemplo em contexto
repository repositório Clone the repository.
commit gravar versão Commit early, commit often.
branch / merge ramo / mesclar Merge the feature branch into main.
conflict conflito Resolve the merge conflict manually.
push / pull enviar / trazer Push your changes before leaving.
stage / staging area preparar / área de preparação Stage the modified files.
remote / origin remoto / apelido do remoto Origin points to the GitHub repository.
untracked não rastreado Untracked files appear in red.
log / diff histórico / diferença The diff shows what changed.
directory / path pasta / caminho Run the command in the project directory.
run / install executar / instalar Run npm install first.

5.6 Palavras de erro

A tabela vive na §4.1 (é a mesma lista). No glossário vivo, esses termos entram no domínio "erros", de preferência colados no print do erro real em que apareceram.

🎯Foco de prova

🎯 Reconhecer é nota de leitura, usar é nota de fala. Bater o olho em constraint e entender já resolve a UC5. Conseguir dizer "I added a UNIQUE constraint to prevent duplicate check-ins" é a UC7. O glossário treina os dois estágios: primeiro a coluna do meio (reconhecer), depois a da direita (a frase inteira, em voz alta).


6. Falar do sistema em inglês

A apresentação da prova é em português. Mas a competência de comunicação técnica oral (UC7) se treina nas duas línguas, e falar do sistema em inglês tem dois retornos diretos: os termos técnicos saem naturais na apresentação (módulo 10 colhe isso), e a competidora fica pronta pra qualquer material ou interação em inglês que a prova trouxer.

6.1 O banco de frases prontas

Frases de verdade sobre o Wedding Pass, prontas pra adaptar ao sistema de cada uma. A meta não é decorar o banco: é falar cada uma em voz alta até sair sem pausa.

O sistema como um todo: - "Wedding Pass is a web system for wedding management." - "It has three user roles: administrator, organizer and reception." - "The front end consumes a REST API connected to a MySQL database."

Banco de dados: - "The database has six tables, and every table has a primary key." - "Each guest belongs to a wedding and may be assigned to a table." - "I added a UNIQUE constraint to prevent duplicate check-ins."

API e segurança: - "The API uses JWT for authentication." - "Every request must send the token in the Authorization header." - "If the token is missing or expired, the server returns 401." - "Passwords are hashed with bcrypt before being stored." - "I used prepared statements to prevent SQL injection."

Front-end: - "The interface is responsive: the layout adapts from mobile to desktop." - "Every screen handles four states: loading, empty, error and success." - "The form validates the input and shows a clear error message."

Funcionalidades: - "When a guest confirms the invitation, the system updates the status and checks the table capacity." - "The check-in uses an atomic update, so the same guest cannot enter twice." - "The dashboard shows the occupation per table, with an alert at 90% of capacity." - "The guest receives a confirmation receipt in PDF."

6.2 O padrão que sustenta qualquer arguição: I chose X because Y

Toda decisão técnica do sistema cabe numa frase desse formato, e é o mesmo músculo do banco de porquês do módulo 10:

O formato força o hábito certo: nunca dizer só o que fez, sempre o que fez e por quê. Em qualquer língua, isso é o que separa descrição (nota 2) de justificativa (nota 3).

6.3 Narrar enquanto codifica 🛠️

A gambiarra de custo zero: durante o treino normal, explicar em voz baixa, em inglês, o que está fazendo. "Now I'm creating the check-in table, with a unique key on the guest id." Sem plateia, sem nota, sem hora marcada. Três frases por dia já mantêm o vocabulário ativo, e é treino de UC7 acontecendo dentro do treino de banco.

6.4 Pronúncia: as palavras que travam

Meta: clareza, não sotaque. A banca quer entender, não avaliar fonética. Mas algumas palavras técnicas têm pegadinhas que fazem a palavra sair irreconhecível, e essas valem 2 minutos de atenção:

Palavra Aproximação Pegadinha
column "cólum" o n final é mudo
foreign "fórin" o g é mudo
unique "iuník" não é "uniquê"
height "ráit" (com h aspirado) não rima com "eight"; rima com "kite"
width "uídth" o th final existe, curtinho
cache "cásh" igual a cash; não é "cachê"
queue "kiú" as 4 últimas letras são mudas
route / router "rut"/"ráut" as duas pronúncias existem; escolher uma e manter
suite "suít" igual a sweet; não é "suáit"
JSON "dgêi-son"
SQL "és-kiu-él" (ou "síquel") as duas valem; "és-kiu-él" é a mais segura
delete "dilít" acento na segunda sílaba
image "ímidj" não é "imêidj"

7. Diluir o inglês no treino (pra não virar matéria)

Inglês técnico morre quando vira "estudar inglês na sexta à tarde". Vive quando se dilui em quatro hábitos que não custam hora de cronograma:

🎯Foco de prova

🎯 A conta da constância. 10 minutos por dia, de agosto até a véspera da Seletiva, são mais de 14 horas de inglês técnico. Somando as duas aulas dedicadas, passa de 20 horas: quase o tamanho de um módulo técnico inteiro, sem gastar um turno sequer do cronograma. É assim que 15% da prova se treina sem ter semana própria.


8. O catálogo de drills de 10 minutos

É daqui que sai o aquecimento de inglês que abre toda aula de todas as guias. Regras da rotina:

Fase do treino Drills que pagam mais
Agosto (banco + API) 1 (domínios banco/Git), 3, 10, 2
Setembro (front + funcionalidades) 1 (domínios front/API), 4, 8, 3
Outubro (lapidação + apresentação) 5, 6, 9, 7
Novembro (reta final) rodízio livre, com peso em 3, 8 e 9

Drill 1 — Palavra do dia

Como roda. O professor escolhe 5 termos do glossário do domínio da semana e mostra só a coluna da esquerda. Cada competidora explica o sentido; depois, cada uma monta uma frase em voz alta usando 2 dos termos, sobre o próprio sistema. Material. O glossário vivo (§5), aberto. Quando paga mais. O tempo todo; trocar o domínio junto com a fase do cronograma.

Drill 2 — Scanning cronometrado

Como roda. Uma página de doc aberta no telão (MDN, MySQL, php.net, Express) e 3 perguntas objetivas. 2 minutos por pergunta, no cronômetro, sem tradutor. Quem acha, fala onde achou (qual seção da página), porque o método importa mais que a resposta. Material. Uma página de doc ligada ao conteúdo da semana + 3 perguntas preparadas (modelo na §3.4). Quando paga mais. Agosto e setembro, quando cada semana traz ferramenta nova.

Drill 3 — Traduz o erro

Como roda. O professor mostra o print de um erro real que apareceu no treino de ontem (ou um da coleção das §4.4/§4.5). As duas aplicam o método dos 30 segundos em voz alta: tipo, mensagem em português, arquivo:linha, hipótese de causa. Sem abrir o código: o drill é de leitura, não de correção. Material. Prints de erros reais coletados durante a semana (o professor vai guardando). Quando paga mais. Sempre; é o drill mais rentável do catálogo, porque treina exatamente o gesto que recupera tempo na prova.

Drill 4 — Status code relâmpago

Como roda. O professor fala um cenário ("login com senha errada", "convidada criada com sucesso", "token expirado", "rota que não existe", "check-in duplicado"), e elas respondem o código e o nome em inglês (401 Unauthorized, 201 Created, 404 Not Found, 409 Conflict). Depois inverte: o professor fala o código, elas dão um cenário do Wedding Pass. Material. A tabela de status codes do módulo 04. Quando paga mais. Setembro, com a API viva e o front consumindo.

Drill 5 — My system in three sentences

Como roda. Cada uma descreve o sistema (ou o módulo da semana) em exatamente 3 frases, em inglês, de pé. Frase 1: o que é. Frase 2: como está construído. Frase 3: um destaque técnico. A outra escuta e diz qual das três ficou mais clara. Material. Nenhum; o banco de frases da §6.1 é a rampa de partida. Quando paga mais. Outubro, esquentando pro módulo 10.

Drill 6 — I chose X because Y

Como roda. Cada uma pega 2 decisões técnicas que tomou no dia anterior (qualquer tamanho: um tipo de coluna, um endpoint, um componente) e as formula em inglês no padrão da §6.2. Vale repetir decisão antiga com formulação melhor. Material. O trabalho de ontem; depois de outubro, o banco de porquês do módulo 10. Quando paga mais. Outubro e novembro, quando defender escolha vira rotina.

Drill 7 — Read aloud

Como roda. Cada uma lê em voz alta 4 ou 5 linhas de uma doc (parágrafo curto ou lista de passos) e resume em português em uma frase. O professor só corrige pronúncia que compromete a compreensão (tabela da §6.4); fluência vem com a repetição. Material. A doc da semana, qualquer trecho instrutivo. Quando paga mais. Outubro e novembro; bom pra variar quando os outros drills cansarem.

Drill 8 — Enunciado relâmpago

Como roda. O professor lê (ou projeta) um mini-requisito em inglês, no estilo do trecho da §2.1. Elas dizem: o que é must, o que é should, o que é may, e o que implementariam primeiro. 2 minutos por requisito, 2 ou 3 requisitos por drill. Material. Requisitos curtos que o professor escreve na véspera, misturando modais e limites numéricos (§2.4) do universo do sistema. Quando paga mais. Setembro em diante; é ensaio direto da leitura de enunciado da prova.

Drill 9 — Arguição relâmpago

Como roda. O professor faz uma pergunta em inglês sobre o sistema ("How does the system prevent duplicate check-ins?", "Where is the capacity rule enforced?", "What happens when the token expires?"). Nível 1: resposta em português usando os termos técnicos certos em inglês. Nível 2: resposta inteira em inglês, 2 ou 3 frases. Começar todo mundo no nível 1; subir pro 2 quando sair sem travar. Material. 3 perguntas preparadas sobre o que foi construído na semana. Quando paga mais. Outubro e novembro, junto do treino de arguição do módulo 10.

Drill 10 — Must, should ou may?

Como roda. O professor mostra 5 frases de spec (uma por vez) e elas classificam em obrigatório, recomendado ou opcional, justificando pela palavra ("must not: proibido, mais grave que faltar um should"). 30 segundos por frase. Misturar pegadinhas: unless, otherwise, up to, at least. Material. Frases da §2 ou variações que o professor escreve. Quando paga mais. Agosto, pra instalar a leitura de modal antes do primeiro TA.


9. Autoavaliação do módulo

  1. Num enunciado, qual a diferença prática entre must, should e may? E por que must not é tão grave quanto um must faltando?
  2. "Each guest may bring up to five companions." Cinco acompanhantes passam na validação ou não? Que palavra define isso?
  3. O que significa unless numa frase de requisito, e qual o erro clássico de implementação que ele provoca?
  4. Qual a diferença entre skimming e scanning, e qual dos dois a prova usa mais?
  5. Qual é a rota dos 30 segundos pra achar uma resposta numa página de doc?
  6. Num stack trace do Node, como separar o que é do seu código do que é caminho interno do framework?
  7. O que diz o erro 1062 Duplicate entry for key 'uq_checkin_convidado', e por que ele pode ser um bom sinal?
  8. Qual a diferença entre x is not defined e Cannot read properties of undefined?
  9. O erro "Unexpected token '<'... is not valid JSON" aparece no front. Onde costuma estar a causa?
  10. Fala, em inglês, três frases sobre o teu sistema: o que é, como é construído, um destaque técnico.
  11. Formula em inglês, no padrão "I chose X because Y", duas decisões técnicas do teu sistema.
  12. Quais são os quatro hábitos que diluem o inglês no treino sem custar hora de cronograma?

Referências do módulo

💡Nota

Inglês rodando por baixo de tudo, 10 minutos por dia? Então falta a última peça, o módulo onde a competidora senta na frente da banca e conta o que construiu: 10 — Apresentação e defesa.

Competições Senac RS · Seletiva 26

Módulo 10

Módulo 10 — Apresentação e defesa

BaseMódulo D do Projeto-Teste (Apresentação Individual, ~2h de slot, até 10 min de fala por competidora)Peso na provamódulo avaliado, com nota própria de julgamentoNaturezaNovo
Ocupação: Desenvolvimento de Sistemas Treinador: Diogo Roehrs
💡Nota

Onde esse módulo entra. É o Módulo D da prova: apresentação individual, até 10 minutos, em português, com pitch técnico e demonstração ao vivo. O cronograma trabalha isso no fim de outubro, depois de todo o sistema já estar lapidado (módulo 08). É a única parte da prova que não é escrever código, e é onde muita gente boa perde ponto por não treinar. Dá pra ter o melhor sistema da sala e apresentar mal: a banca avalia o que você mostra e conta, não o que está na sua cabeça. O sistema já está impecável; esse módulo transforma o impecável em nota na frente da banca. Natureza 🆕 novo, porque exige uma habilidade diferente de programar: comunicar sob pressão, no tempo, com o sistema rodando ao vivo.


1. O que a banca realmente avalia

Não é teatro nem discurso decorado. A apresentação é uma prova de julgamento (J), então a banca pontua de 0 a 3 (o mesmo descritor de todo o resto da prova), e a diferença entre o 2 e o 3 é exatamente onde esse módulo joga. Três coisas estão sendo medidas ao mesmo tempo:

Como isso vira nota de julgamento, na prática:

Nota Como a apresentação se parece
0 Não apresenta, ou lê a tela sem explicar nada. Não demonstra o sistema.
1 Mostra telas soltas, descreve "aqui tem um cadastro, aqui uma lista", sem dizer por que nem como funciona por dentro. Estoura ou desperdiça o tempo.
2 Demonstra o fluxo funcionando e descreve o que faz. Fala com clareza, mas fica no "o quê": não justifica decisões nem mostra o que acontece por baixo.
3 Conduz uma demo em forma de história, narra o fluxo de dados pelas camadas, justifica as decisões técnicas com "porquês", administra o tempo e responde à arguição com segurança, inclusive quando não sabe.
🎯Foco de prova

🎯 Isso é a UC7 na prática. "Elaborar orientações técnicas orais" é exatamente isto: falar de tecnologia de um jeito que o outro entende. É a mesma competência do inglês oral do módulo 09, agora em português e com o próprio sistema na frente. Quem treinou o vocabulário técnico do 09 chega aqui com meio caminho andado, porque os termos certos (endpoint, constraint, hash, componente) saem naturais.


2. A anatomia do Módulo D

Antes do roteiro, entender a logística, porque ela muda como você prepara:

⚠️Pega-ratão

⚠️ Pega-ratão de logística: chegar no Módulo D com o sistema do jeito que ficou no Módulo C, sem preparar o palco. Banco vazio, servidor caído, aquele bug pequeno que você ia arrumar "depois". A apresentação não é só falar: é falar com o sistema pronto pra brilhar. Meia hora de preparo de palco vale mais que meia hora de ensaio de fala.


3. A estrutura dos 10 minutos

Dez minutos evaporam. Sem estrutura, o tempo some numa parte só e falta pro resto (quase sempre falta pros destaques técnicos, que é justo o que mais pontua). Uma divisão que funciona, com marcos de relógio pra você saber se está no ritmo:

Tempo Parte O que fazer
0:00–0:30 Abertura O que é o sistema e pra quem serve. "O Wedding Pass gerencia um casamento, da lista de convidados ao check-in na porta." Situa a banca em uma frase.
0:30–1:00 Panorama técnico A stack e a arquitetura em uma pincelada. "Front em HTML, CSS e JavaScript, uma API no back e banco MySQL, separado em camadas."
1:00–5:30 Demo da jornada O sistema funcionando de verdade, seguindo a jornada real de um convidado. É o coração (§4).
5:30–8:00 Destaques técnicos As decisões das quais você se orgulha, com o porquê (§6). É o trecho que mais separa 2 de 3.
8:00–9:00 Honestidade e visão O que daria pra evoluir, o que não deu tempo mas está preparado pra receber.
9:00–9:30 Fechamento O que o sistema resolve, em uma frase.
9:30–10:00 Buffer Folga pra imprevisto. Se não usar, ótimo, sobra pra respirar.
🛠️Gambiarra boa

🛠️ Gambiarra boa: um marco na cabeça. Você não precisa cronometrar as sete linhas. Precisa de um marco âncora: "aos 5 minutos e meio eu tenho que estar saindo da demo pros destaques". Se nesse ponto você ainda está demonstrando, corta e avança. Um único marco no meio segura o ritmo inteiro sem você virar refém do relógio.


4. A demo que conta uma história

O erro clássico é mostrar tela por tela solta: "aqui é o login, aqui a lista, aqui o cadastro". Vira uma lista de features, e a banca desliga. A demo boa segue a jornada de um convidado, do convite até a festa, e o sistema aparece como um todo coeso.

O roteiro da jornada no Wedding Pass:

  1. Entra como organizadora (login). Já mostra que tem autenticação e perfis.
  2. Cadastra uma convidada e a coloca numa mesa. Mostra o CRUD e a máscara de telefone funcionando enquanto digita.
  3. Gera o convite com código único e "envia". Mostra a regra de negócio.
  4. Abre a página pública de RSVP (como se fosse a convidada no celular) e confirma presença. Mostra o fluxo que sai do sistema e volta.
  5. Vira recepcionista e faz o check-in na porta. É o clímax técnico: é onde mora a proteção contra dupla entrada.
  6. Fecha no dashboard, com os números se movendo (confirmados, presentes, ocupação das mesas). Mostra o todo funcionando.
🛠️Gambiarra boa

🛠️ Gambiarra boa: a demo é uma narrativa, não um tour. Seguir uma convidada do convite à festa faz a banca acompanhar uma história com começo, meio e fim, em vez de decorar um menu. Fica mais claro, mais natural, e prova que o sistema é um organismo, não pedaços costurados. Escolher uma convidada de nome fácil ("vou seguir a Marina aqui") e levá-la até o fim amarra tudo.

⚠️ Pega-ratão: demonstrar cadastrando na hora, do zero, com o banco vazio. Cadastro ao vivo trava, erra digitação, e o dashboard no fim aparece vazio, sem impacto. A demo nasce de um banco já povoado (o seed do módulo 03); você cadastra uma convidada nova em cima de dezenas que já estão lá, e o dashboard fecha cheio.


5. Narrar o fluxo de dados: provar que entende por dentro

O que separa "sei usar meu sistema" de "sei como meu sistema funciona" é narrar o caminho do dado. Durante a demo, ao executar cada ação, dizer em voz alta, em uma frase, o que acontece por baixo. São 15 segundos que provam que você enxerga as camadas trabalhando juntas (o que os módulos 03 a 07 construíram). Frases prontas pra adaptar:

No login:

💡Nota

"Quando eu entro, o back confere a senha comparando com o hash guardado no banco, nunca com a senha em texto, e devolve um token que identifica meu perfil nas próximas telas."

No cadastro da convidada:

💡Nota

"Esse telefone está formatado na tela pela máscara, mas o que vai pro banco é só o número limpo. E a validação não está só aqui no front: se alguém chamar a API direto, o back barra do mesmo jeito."

No RSVP público:

💡Nota

"Essa página não pede login, ela abre pelo código único do convite. A convidada confirma, e o status daquele convite muda de pendente pra confirmado no banco, protegido por um ENUM que só aceita esses valores."

No check-in (o clímax):

💡Nota

"Aqui é o ponto sensível. Se duas recepcionistas escaneiam a mesma convidada no mesmo segundo, o sistema não pode registrar duas entradas. Eu garanto isso em duas camadas: uma verificação atômica no back e uma constraint UNIQUE no banco como última muralha. Mesmo que o back falhe, o banco não deixa passar."

No dashboard:

💡Nota

"Esses números não são chute nem contagem no front: são agregações que o próprio banco calcula com GROUP BY e devolve prontas. Se a festa tem 400 convidadas, a conta é do MySQL, não do navegador."

🎯 É aqui que os "detalhes de nível 3" pagam. A condição de corrida (módulo 04), o CASCADE vs RESTRICT (módulo 03), a validação no back (módulo 04), a lista branca no ORDER BY (módulo 08): a banca talvez nem teste esses detalhes na correção do código. A apresentação é onde eles viram ponto, porque você conta que pensou neles. Guardar dois ou três desses pra soltar durante a demo é ouro puro de julgamento.


6. O banco de porquês: "por que" vale mais que "o que"

Qualquer um descreve o que fez. Nível 3 é explicar por que. O hábito mais valioso da apresentação é completar, pra cada parte do sistema, a frase "eu fiz assim porque...". Esse é o banco de munição pra demo e, principalmente, pra arguição. Cada linha é uma decisão real dos módulos 03 a 08 com o porquê pronto pra falar:

Decisão (módulo) O porquê, pra soltar na hora
DECIMAL(10,2) pra valores em dinheiro (03) "Porque FLOAT arredonda errado e dinheiro não pode arredondar. Um centavo perdido em relatório é erro visível."
ENUM pro status do convite (03) "Porque o banco garante que só entra 'pendente', 'confirmado' ou 'recusado'. Valor inválido nem chega a ser gravado."
Constraint UNIQUE no check-in (03, 07) "Porque é a última muralha contra dupla entrada. Mesmo que o código acima falhe, o banco não deixa duas presenças pro mesmo convidado."
Verificação atômica no check-in (04) "Porque duas recepcionistas escaneando junto criam uma condição de corrida, e eu resolvo no back antes do banco precisar barrar."
Hash de senha, bcrypt / password_hash (04) "Porque se o banco vazar, a senha não vaza junto. Ninguém, nem eu, consegue ler a senha original."
Validação também no back (04) "Porque validar só no front protege quem usa a tela, não quem chama a API direto. O back nunca confia no que chega."
Prepared statements (04) "Porque concatenar valor em SQL abre injection. Com parâmetro preparado, o valor nunca vira comando."
Autorização por perfil no middleware (07) "Porque esconder o botão no front não impede a chamada. Quem barra de verdade é a checagem de perfil na rota."
Arquitetura em camadas / MVC (04) "Porque separar responsabilidade deixa fácil achar bug e mexer numa parte sem quebrar a outra."
Tokens de cor no :root (06) "Porque a identidade visual muda num lugar só. Trocar a cor da marca é uma linha, não cinquenta."
Os quatro estados de tela (05) "Porque tela branca no carregamento parece travada. Carregando, vazio, erro e sucesso fazem o usuário sempre saber o que está rolando."
Componente de confirmação reusável (08) "Porque ação destrutiva sem confirmar é acidente esperando acontecer, e um componente só mantém o padrão em todo lugar."
🎯Foco de prova

🎯 Todo "o quê" ganha um "porque". Treinar isso é pegar cada parte do sistema e completar "eu fiz assim porque...". Numa apresentação de nível 3, praticamente toda frase de destaque técnico carrega um porquê. É o que transforma uma descrição passiva numa defesa de decisões, que é o que a banca quer ouvir de quem sabe o que está fazendo. Leve esse banco decorado; ele é 80% da arguição.


7. A arguição: perguntas prováveis e como responder

Depois da fala, a banca pergunta. Não é pegadinha, é conferir se o que você mostrou você entende. As perguntas quase sempre saem do banco de porquês (§6), então quem decorou aquilo já tem as respostas. As mais prováveis, com um modelo de resposta:

Quando você não sabe a resposta: não inventa e não congela. O protocolo:

💡Nota

"Isso eu não cheguei a implementar / não tenho certeza agora, mas o caminho que eu seguiria é [o raciocínio]."

Mostrar o raciocínio mesmo sem a resposta pronta vale muito mais que chutar. A banca distingue na hora quem pensa de quem decorou, e um "não sei, mas resolveria assim" é resposta de nível 3. Fingir que sabe e errar é o pior dos mundos.

⚠️Pega-ratão

⚠️ Pega-ratão: responder defensivo, como se a pergunta fosse ataque. A arguição é chance de ganhar ponto, não julgamento pessoal. "Boa pergunta, foi uma escolha consciente: eu fiz X porque Y" é uma resposta que vira a pergunta a seu favor.


8. A demo ao vivo: o que dá errado e o plano B

Demo ao vivo é a parte mais arriscada da prova inteira: pode travar, dar erro, faltar dado, cair a conexão. Não dá pra eliminar o risco, dá pra ensaiar o contorno. Blindagem em quatro camadas:

⚠️Pega-ratão

⚠️ Pega-ratão que já custou competição: improvisar a demo na hora, "porque eu conheço meu sistema". Conhecer o sistema não é conhecer a apresentação dele. O nervoso encurta o raciocínio, o tempo foge, a ordem embola. Demo boa é demo ensaiada, e a banca perdoa um bug ao vivo, não perdoa quem trava junto com ele.


9. Gestão do tempo e postura na fala

O tempo. O limite é rígido. Estourar é problema; deixar metade da nota por gastar tempo demais numa parte também. Ensaiar com o relógio na frente, umas três vezes, calibra o ritmo. Você descobre onde gasta demais (quase sempre a abertura e a demo) e onde corre (quase sempre os destaques técnicos, que é o que mais pontua). O marco âncora dos 5 minutos e meio (§3) resolve 90% disso.

A postura. Não precisa ser orador, precisa ser claro e seguro:

🎯Foco de prova

🎯 Confiança tranquila é o tom. Nem arrogante, nem inseguro. Alguém que construiu algo, entende o que construiu e está contando com clareza. A banca sente isso, e a nota de julgamento reflete. Segurança não é falar bonito, é falar sabendo por quê.


10. Checklist do dia da prova

Antes de abrir a boca, o palco tem que estar montado. A lista mínima:


11. Como o CIS enxerga a apresentação

A apresentação é transversal: é o único momento em que você toca todos os critérios de uma vez, porque narra o sistema inteiro. O que a banca de julgamento observa, e onde cada coisa pontua:

O que a banca observa Onde vira nota
Demonstra o sistema funcionando ponta a ponta Funcionalidade, robustez
Narra o fluxo de dados pelas camadas Domínio técnico, back e banco
Justifica decisões com "porquês" Julgamento técnico (o coração do 2→3)
Responde à arguição com segurança, inclusive o "não sei" Maturidade técnica, comunicação
Administra o tempo e conduz com clareza Comunicação (UC7)
Usa o vocabulário técnico certo Domínio, ligação com o inglês (módulo 09)
🎯Foco de prova

🎯 O 2 vira 3 aqui de novo, e de forma mais barata. No código, subir de 2 pra 3 custa horas de lapidação. Na apresentação, custa preparo: um roteiro ensaiado, doze porquês decorados, um backup no celular. É a hora da prova com a melhor relação esforço/ponto de todas, e é a que mais gente ignora. Quem trata o Módulo D com o mesmo respeito que trata o código sai na frente.


12. Autoavaliação do módulo

Responder em voz alta, de preferência apresentando de verdade pra alguém com o cronômetro na mão:

  1. Quais as três coisas que a banca avalia na apresentação, e como isso vira nota de 0 a 3?
  2. Como é a logística do Módulo D (tempo de fala, idioma, arguição)?
  3. Descreve a estrutura dos 10 minutos com os marcos de tempo. Qual é o marco âncora?
  4. Por que seguir a jornada de uma convidada é melhor que mostrar tela por tela?
  5. Narra, em uma frase cada, o que acontece por baixo no login, no check-in e no dashboard.
  6. Escolhe cinco decisões técnicas do teu sistema e completa "eu fiz assim porque..." pra cada.
  7. Como você responde quando a banca pergunta algo que você não sabe?
  8. Quais as quatro camadas de blindagem da demo ao vivo?
  9. O que precisa estar pronto no palco antes de você começar a falar?
  10. Por que a apresentação é a hora da prova com a melhor relação esforço/ponto?

💡Nota

Fim da apostila. As dez peças estão na mesa: como a prova funciona, o ambiente, o banco, o back, o front, a UX, as funcionalidades novas, a lapidação, o inglês e a apresentação. A teoria fez a parte dela. Agora é o que apostila nenhuma faz sozinha: treinar, treinar, treinar, e usar a autoavaliação de cada módulo pra transformar teoria em nível 3. Quem chegou até aqui construiu um sistema impecável e sabe contar por que cada peça está onde está, em português e em inglês. É isso que a banca chama de nível 3. Volta pro índice sempre que precisar situar onde a gente está no cronograma.


Referências do módulo